Skip to content

feat(web-shell): inspect session tool calls by prompt - #12466

Merged
wenshao merged 21 commits into
QwenLM:mainfrom
ytahdn:feat/web-shell-turn-calls-panel
Sep 24, 2026
Merged

wenshao merged 21 commits into
QwenLM:mainfrom
ytahdn:feat/web-shell-turn-calls-panel

Conversation

@ytahdn

@ytahdn ytahdn commented Sep 22, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

Adds a session-scoped Tool calls panel opened from a user message. The selected prompt is preselected, with a prompt selector, refresh action and tool-type filter. Rows show localized tool names, descriptions, status, MCP badges and recorded timing. Expanded rows render JSON arguments/results, structured shell output and file diffs, with links to file and agent detail tabs. Panel selection survives reloads.

Running prompts use the existing message stream without polling; historical prompts use a workspace-scoped, complete-turn read API. Tool start timestamps are retained through recording and replay instead of inferring execution times from browser receipt or batch logging. Embedded hosts opt in with showToolCalls; the standalone page enables it.

Why it's needed

Inspecting a prompt currently requires finding tool calls scattered through the conversation. Reading only the live message window also misses historical calls and can associate a selected prompt with the wrong projection. This panel keeps stable prompt/call identities and reports unavailable or incomplete history explicitly.

Reviewer Test Plan

How to verify

  1. Open Tool calls from a user message. Confirm its prompt is selected, descriptions and statuses match the message area, and the tool-type filter narrows the rows without changing the total count.
  2. Expand shell, MCP and file-edit calls. Confirm command/result/other separation, JSON formatting, localized names, file diffs, and file/agent tabs opening after Tool calls.
  3. Select a running prompt and then an older prompt. Confirm live updates do not poll history, historical reads use the selected prompt, and settlement preserves rows and expansion.
  4. Refresh the panel and reload the page. Confirm the selected prompt and dock restore, duplicate live/persisted prompt entries are merged, and failed reads show a retryable notice.
  5. Hover a completed duration with recorded timing. Confirm start/end timestamps agree with its duration. Older records with missing timestamps must not show fabricated values. Embedded hosts should have no entry unless they enable showToolCalls.

Evidence (Before & After)

Before: no consolidated session tool-call inspector. The initial local panel relied on live blocks and could omit historical calls or show receipt times as execution times. No controlled screenshot of the pre-feature baseline is claimed.

After: the sender path runs in Chromium against the real local daemon and bundled Web Shell, with a local scripted OpenAI-compatible model and real shell/glob execution. Running selection persists, settlement reads history once, reopening the original sender message adopts its record ID, and page reload restores the panel. No mock daemon or browser response interception; no production-model or Git-operation claim. The running screenshot follows a manual index refresh that clears the new-empty-session notice documented in the report.

Sender selected while running Sender panel restored after reload
Sender selected while running Sender restored after reload

Latest merge verification (2e424efa8e): 1,379 scoped Web Shell tests passed across App, the standalone entry, transcript viewport, conversation search, Tool calls and artifact/trajectory panels. Full build/typecheck/bundle and commit hooks passed. Chromium with a real daemon and local scripted model verified sender selection, historical timing, panel restoration and tool-span selection in the overview. Conversation search is covered by unit tests; no browser search verification is claimed. The initial empty-session index notice remains documented, not claimed fixed. Browser evidence and limits. Earlier cross-package validation remains in the PR comments.

Upstream trajectory overview after merge

Tested on

OS Status
🍏 macOS ✅ Build, typecheck, unit tests and Chromium real-daemon sender E2E
🪟 Windows ⚠️ Not tested locally
🐧 Linux ⚠️ Not tested locally

Environment (optional)

macOS, Node.js 22.14.0, pnpm 11.24.0, bundled Web Shell and Chromium with an isolated real daemon and local scripted model. Based on origin/main at 906418aa9b; structured shell results from #12311 are already in the base branch.

Risk & Scope

  • Main risk or tradeoff: this feature crosses shared scheduler/telemetry, ACP replay, SDK and Web Shell code, so maintainer review of timing compatibility and workspace ownership is requested. The new GET route is persisted-workspace scoped and uses the resolved runtime, snapshot bounds and existing redaction. The public API has no pagination; over-budget or incomplete reads fail explicitly.
  • Not validated / out of scope: production-model behavior, a repeat of the maintainer’s large-session survey, actual terminal interaction, Windows and Linux locally. Agent details load on demand; child-agent transcripts are omitted from the summary list. Timing preserves the existing duration scope, including approval/scheduling wait.
  • Breaking changes / migration notes: no migration; added timing fields are optional, old records remain readable, and embedded hosts default to hiding the new entry.

Design: English · 简体中文. Both versions describe the same behavior, constraints and acceptance criteria.

Linked Issues

No linked issue. Builds on the structured shell result contract from #12311.

中文说明

本 PR 的改动

新增从用户消息打开的会话级“工具调用”面板。默认选中入口所属提示词,提供提示词选择、刷新及工具类型筛选。每行展示国际化工具名、描述、状态、MCP 标签和记录的耗时;展开后展示 JSON 参数与结果、结构化命令输出及文件 diff,并可打开文件和智能体详情页签。刷新页面后保留面板选择。

运行中的提示词复用消息流,不轮询历史;历史提示词通过工作区限定的接口读取完整轮次。工具开始时间贯穿记录与回放,不再用浏览器接收时间或批量写日志时间推测。嵌入宿主通过 showToolCalls 显式开启入口,独立页面默认开启。

为什么需要

检查一个提示词的工具调用,目前需要在对话中逐条查找。仅使用实时消息窗口还会漏掉历史调用,或将选中的提示词匹配到错误的投影。本面板使用稳定的提示词及调用标识,并明确报告无法获取或不完整的历史。

评审验证计划

如何验证

  1. 从用户消息打开“工具调用”。确认默认选中该提示词,描述与状态和消息区一致,类型筛选缩小展示范围但不改变总调用数。
  2. 展开命令、MCP 和文件编辑调用。确认命令/结果/其他分区、JSON 格式、国际化名称及文件 diff,文件和智能体详情页签应位于“工具调用”之后。
  3. 在运行中的提示词与历史提示词之间切换。确认实时更新不轮询历史,历史查询绑定所选提示词,执行结束时保留行和展开状态。
  4. 刷新面板及页面。确认提示词选择与面板恢复,实时和已持久化的同一提示词不重复,读取失败显示可重试提示。
  5. 悬停具有完整记录的已结束耗时,确认开始/结束时间与耗时一致。旧记录缺失的时间不得推测展示。嵌入宿主未开启 showToolCalls 时不展示入口。

证据(前后对比)

之前:没有统一的会话工具调用检查面板。最初的本地面板依赖实时块,会漏掉历史调用或把接收时间当作执行时间;不声称提供受控的功能开发前截图。

之后:在 Chromium 中使用真实本地 daemon 与打包 Web Shell验证发送端链路,模型为本地脚本化 OpenAI 兼容服务,shell/glob 工具真实执行。运行中保存选择,结算后读取一次历史,从原发送消息重新打开会获取 record ID,刷新页面恢复面板。未使用模拟 daemon 或浏览器响应拦截,不代表生产模型或 Git 操作验证。运行中截图拍摄于手动刷新索引清除空会话初始提示后,详情见报告。

运行中选中发送端提示词 刷新后恢复发送端面板
运行中选中发送端提示词 刷新后恢复发送端面板

最新合并验证(2e424efa8e):1,379 项 Web Shell 相关测试通过,覆盖 App、独立入口、历史消息视口、会话搜索、工具调用及扩展区/轨迹面板;完整构建、类型检查、打包及提交钩子通过。Chromium 使用真实 daemon 和本地脚本模型,验证发送端选择、历史计时、面板恢复及时间轴工具区间选中。会话搜索由单测覆盖,不声称完成浏览器搜索验证。空会话初始索引提示仍单独记录,不声称已修复。浏览器证据与验证边界。此前跨包验证保留在 PR 评论中。

合并后的主分支轨迹时间轴

测试平台

操作系统 状态
🍏 macOS ✅ 构建、类型检查、单测及 Chromium 真实 daemon 发送端 E2E
🪟 Windows ⚠️ 未本地测试
🐧 Linux ⚠️ 未本地测试

环境

macOS、Node.js 22.14.0、pnpm 11.24.0、打包 Web Shell、Chromium、隔离的真实 daemon 及本地脚本模型。已合并 origin/main 的 906418aa9b;#12311 的结构化命令结果已在基分支中。

风险与范围

  • 主要风险或取舍:功能涉及共用调度器/遥测、ACP 回放、SDK 和 Web Shell,需要维护者关注计时兼容性与工作区归属。新增 GET 接口限定于持久化工作区,复用已解析 runtime、快照边界及脱敏规则。公共接口不分页,超限或不完整读取明确报错。
  • 未验证或范围外:生产模型行为、维护者大会话验证的重复执行、实际终端交互,以及本地 Windows 和 Linux 测试。智能体详情按需加载,摘要列表省略子智能体完整记录。耗时沿用既有口径,包含审批与调度等待。
  • 破坏性变更及迁移:无需迁移;新增计时字段可选,旧记录保持可读,嵌入宿主默认隐藏新入口。

设计文档:English · 简体中文。两份文档的行为、约束和验收标准保持一致。

关联问题

无关联 issue。基于 #12311 已提供的结构化命令结果约定。

钉萁 and others added 14 commits September 20, 2026 14:25
Both sides added a distinct `paths` entry at the same spot in
integration-tests/tsconfig.json: this branch mapped
`@qwen-code/qwen-code-core/shellResult` and main's QwenLM#12304 mapped
`@qwen-code/qwen-code-core/telemetryConstants`. Keep both; the map has no
ordering constraint. Every other overlapping file auto-merged as an exact
union.
)

- Documents: stop duplicating the command above the fallback text and
  label an empty legacy result as 'No output' (maintainer N1/N2)
- Escape control and bidi characters at every shell-card render site
  while keeping clipboard copies byte-exact
- Render a string rawOutput display instead of model-facing content so
  background-promotion and refusal messages stay visible
- Show a finished command's elapsed time from startTime/endTime
- Suppress the Command section when the legacy envelope already leads
  with the same command, keeping it once per expanded card
- Pin the wasCancelled status, the background timeout gates, the
  synthetic exit-zero headline and the status icons; freeze the
  running-elapsed fixture's clock
- Tag the card smoke test and wait for the SSE connection before
  driving live frames
- Drop the dead .expandedBash selector and align both design docs with
  the undisclosed live-frame transport for line/byte counts

Co-authored-by: Qwen-Coder <[email protected]>
The deterministic verification gate ran the core suite on a persistent
runner whose real ~/.gitconfig carries remote.pushDefault=gd, and the
three gitPush tests that resolve the push remote failed with
'fatal: gd does not appear to be a git repository'. gitEnv strips only
the GIT_CONFIG_* env overrides, so the code under test still honors the
host HOME config — the exact gap the file's hermeticEnv() helper
documents and every gitPull test already guards against.

Pass hermeticEnv() at all six gitPush call sites so host config can no
longer steer push resolution. Reproduced with a poisoned HOME
(remote.pushDefault=gd): the three gate failures appear on the pre-fix
tree and the file is 107/107 green after, in both clean and poisoned
environments.

Co-authored-by: Qwen-Coder <[email protected]>

Co-authored-by: Qwen-Coder <[email protected]>
…LM#12311)

The deterministic verification gate rebuilds core with a scoped
npm run build --workspace packages/core, but the review-address job
installs with QWEN_SKIP_PREPARE=1 and restores only the root and core
dist artifacts, so packages/browser-use/dist is absent and the core
build dies in copyBrowserUseAssets. The dependency on the built
browser-use runtime was only expressed through the root build's
ordering, leaving every scoped core build (the gate's, or a
developer's on a prepare-skipped install) broken.

Add a prebuild that stages the browser-use runtime only when its
dist/index.js sentinel is missing. The root build keeps building
browser-use first, so the prebuild is a no-op skip there and the
fallback fires exactly when the runtime is absent.

Probe matrix: scoped core build fails without the runtime pre-fix
(the gate rejection, reproduced locally), passes with the runtime
absent (fallback stages it), and passes unchanged with the runtime
present (skip path).

Co-authored-by: Qwen-Coder <[email protected]>
…enLM#12311)

A persisted empty display string from a silent command is falsy, so the
card fell back to the model-facing envelope in content and the
legacyOutputRepeatsCommand guard then suppressed the Command panel,
rendering a bare envelope with no copy action. Gate the string branch on
the type alone so an empty string renders as empty output next to the
command.
…alls-panel

# Conflicts:
#	packages/web-shell/client/components/artifacts/ArtifactPanel.tsx
#	packages/web-shell/client/components/messages/ToolGroup.tsx
#	packages/web-shell/client/i18n.tsx
#	packages/web-shell/client/main.tsx
@ytahdn

ytahdn commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator Author

Post-merge E2E report

Commit: d10c76801c30a743674ac5b53e2833e16b1a9648. Chromium frontend E2E: 2/2 passed, light and dark, no page errors.

  • Verified entry, localized descriptions, statuses including cancelled 0ms, recorded start/end tooltip, MCP filtering and 11px badge, separate JSON arguments/results, explicit Refresh and browser reload restoring the open panel and durable prompt identity.
  • Explicit Refresh added exactly one tool-calls read in each theme. This capture pass did not test a live-running clock.
  • Actual merged Web Shell rendered synthetic events and deterministic history responses from a mock daemon. This does not validate real tool execution, real daemon persistence, MCP authentication or Git backend operations.
  • Screenshots were visually inspected and stored separately from the implementation on ytahdn/qwen-code:assets/web-shell-turn-calls-panel, fixed commit 16b4d185237849a2438054e0d50cf45f2695f8b2.

Full browser report

Build and unit validation

  • Frozen-lockfile install, full build/bundle, root typecheck and commit-hook formatting/ESLint passed.
  • 4,510 scoped checks passed: Core 586; ACP replay 136; SDK 878; Web Shell 1,676; CLI Session/replay/telemetry/read helpers 1,231; new tool-calls route cases 3. Duplicate reruns are not included in this total.
  • The first broad server run was invalidated by sandbox restrictions on binding local ports. Outside that sandbox, full-file runs completed with 1,321/1,322 and 1,319/1,322 passing respectively. Failures moved between a legacy resume status assertion, a socket hang-up, an unarchive authentication assertion and a request-rejection status assertion. The original legacy load/resume cases passed 2/2 in isolation, and the original failing resume case passed on the second full run.
  • A single comparison using the original origin/main (c83265ff36) server test and session route in a temporary import-preserving overlay passed 1,319/1,319. It reused current built dependencies; this was not a clean checkout build. Therefore this does not establish an existing main failure, nor does it establish a deterministic regression in this feature. The full server suite is explicitly not reported as consistently green; this broader HTTP-test instability remains unresolved. No unrelated source changes were made to silence it.
中文验证说明

合并提交 d10c76801c30a743674ac5b53e2833e16b1a9648 的 Chromium 前端 E2E 2/2 通过,覆盖浅色和深色,无页面错误。

  • 已验证入口、国际化描述、包含取消 0ms 的状态、记录起止时间的 tooltip、MCP 筛选与 11px 标签、独立的 JSON 参数/结果、手动刷新,以及页面重载恢复面板和持久化提示词标识。
  • 每种主题的手动刷新均只增加一次工具调用读取。本次截图验证没有覆盖运行中计时递增。
  • 使用真实已合并的 Web Shell 界面,事件和历史响应来自模拟 daemon。不代表真实工具执行、真实 daemon 持久化、MCP 认证或 Git 后端操作验证。
  • 截图已目视检查,保存在 ytahdn/qwen-code:assets/web-shell-turn-calls-panel 独立素材分支,固定提交 16b4d185237849a2438054e0d50cf45f2695f8b2,未进入实现分支。

完整浏览器报告

构建与单元验证

  • 固定锁文件安装、完整构建/打包、根目录类型检查及提交钩子的格式/ESLint 检查通过。
  • 本次范围内 4,510 项检查通过:Core 586、ACP 回放 136、SDK 878、Web Shell 1,676、CLI Session/回放/遥测/读取 helper 1,231、新增工具调用路由 3;不重复计入重跑用例。
  • 首次完整 server 运行受沙箱禁止监听本地端口影响,不作为有效全量结果。解除该限制后,两次完整运行分别为 1,321/1,322 和 1,319/1,322 通过;失败在旧会话 resume 状态断言、socket hang-up、取消归档认证断言和请求拒绝状态断言之间漂移。原旧会话 load/resume 用例单独运行 2/2 通过,原失败的 resume 在第二次完整运行中也通过。
  • 使用 origin/main(c83265ff36)原始 server 测试及 session route,在保持相对导入定位的临时 overlay 中进行了一次对照,1,319/1,319 通过。对照复用了当前构建依赖,并非干净检出的完整构建。因此既不能声称已证明主分支已有失败,也不能确认本功能存在确定性回归。完整 server 套件明确未记为稳定全绿,更广的 HTTP 测试不稳定性尚未归因;未为消除这些失败而修改无关源代码。

@wenshao

wenshao commented Sep 22, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification: real daemon, real tool execution, Linux (d10c768)

Verdict: I'd merge after one small client fix, or with it as an immediate follow-up. The new server read is correct end to end. Against a real qwen serve daemon, its results match the raw JSONL call-for-call across 217 real tool calls, including after a daemon restart. The recorded timings check out against wall-clock. The panel works in the production Web Shell build. I found one defect, on the path users hit most often: the client that sent the prompt. If you open Tool calls from your own message, the tab has no durable identity. That prompt appears twice in the prompt selector, the tab is never persisted, so a page reload closes the panel, and no history read happens after the turn settles. The root cause is below, along with a 12-line fix and two tests. I verified the fix in the same browser rig.

How I tested (environment)
  • Linux x86_64, Node 22.22.2. pnpm install --frozen-lockfile, a full npm run build and npm run bundle for head d10c768 and base c83265ff36.
  • A real qwen serve daemon serving its own bundled Web Shell (the production dist/web-shell, not vite dev). Chromium 1228 via Playwright. No page.route, no mock daemon.
  • A scripted OpenAI-compatible model that issues real tool calls, which the daemon executes: shell, read/edit/write, grep/glob, a failing read, a non-zero exit, parallel batches of 10, a subagent, and a duplicate provider id. Two real stdio MCP servers: one deferred (reached through tool_search → tool_call) and one with alwaysLoadTools.
  • Approval ran through a real permission request. The daemon voted after a 1.5 s wait. Cancellation used POST /session/:id/cancel. For the crash case I sent kill -9 to the daemon mid-call.
  • The daemon was restarted between seeding and browsing, so reads take the cold path from disk.

What holds (verified by execution)

Area Evidence Result
Completeness and ownership 9 turns, 217 calls, compared against the raw chats/<id>.jsonl. Includes a 90-call turn and a 110-call turn stopped by the loop cap, the "57 shown as 13" class. Count, order and ids match exactly per turn. 0 missing, 0 extra, 0 duplicated. Identical after a daemon restart. sessions/live-state stays [], so the read never attaches the session.
Recorded timing Each fake-model response logs its send time and the arrival time of the next request. Every one of the 206 timed calls has [startedAt, startedAt+durationMs] inside its real wall-clock window, and equals the raw ui_telemetry started_at/duration_ms (206/206). The tooltip is exact to the millisecond (19:59:19.428 → .452 for 24 ms). The approval wait is included (3548 ms for a 2 s command with a 1.5 s approval), as the design doc states.
Legacy records A session recorded by the base daemon, read by head. 11 durations and 0 startedAt. No tooltip or aria-description: nothing is fabricated.
Statuses Tool error, non-zero exit, duplicate provider id, loop-cap rejection, cancel after 1.5 s, and kill -9 of the daemon mid-call. All show Failed with the right reason. Cancel shows 2s Cancelled: the recorded cancel overrides the generic failed replay. After the crash and a restart, the dangling call becomes Failed: "Tool result missing from saved history…".
Post-approval ACP frame SSE capture. tool_call_update(in_progress) arrives 3 ms after permission_resolved, carrying rawInput and startedAt.
Live prompt A real 8 s shell call sent from the composer. The clock ticks 948 ms → 8 s with 0 /tool-calls reads while running. A mid-turn message injected at +5 s does not split the turn: both calls stay under the prompt, and there is no selector entry for the injection.
Read economy page.on('request'). Selecting a prompt = 1 GET. Refresh = 1 GET. Reload restore = 1 GET. Viewing an older prompt while another runs = 1 GET, with no repeats over 4 s of streaming. Switching back to the running prompt = 0 GETs while it runs and 1 at settle, and the rows don't flash.
UI EN/zh-CN, light/dark. Localized names. MCP badge and filter (the count stays 11). Shell Arguments = command, Result = output, Other collapsed. Edit shows the recorded diff. JSON is formatted.
API contract curl. 401 without a token or with a wrong one. 400 invalid_turn_anchor for a missing, blank, 201-char or repeated turnId, an unknown uuid, a non-prompt record, or another session's record. 404 when the session is read through a different registered workspace (no cross-workspace read). The route is a 404 on base.
Tests (Linux) Local vitest. core 586, acp-bridge 136, cli 1231, server.test.ts full file 1322/1322 on the first run (it was flaky on macOS for the author), sdk 878, web-shell 1470 (10 touched files). All green. CI is green, including Test.

entry
panel on a real daemon
row details
live
zh-CN and 110 calls

Defect: a tab opened from your own message has no durable identity

Repro: send a prompt from the composer, then click View tool calls on that message, either while it runs or after.

PR head With the fix below
The prompt in the selector listed twice once
turn_calls tab in localStorage none (dropped by serializeArtifactPanelTabs) {promptId}
/tool-calls read after settle 0 1
Panel after a page reload closed restored, same prompt

A second client watching the same session behaves correctly on head: it gets promptId from the bridge echo. So this only affects the sender, which is the common case.

sender tab A/B

Root cause. The sender's own user echo is suppressed (suppressOwnUserEcho). As DaemonSessionProvider.tsx:362-364 notes, that local block never gets a recordId, and the tab that App.openTurnCalls builds from it ends up with neither recordId nor promptId. The persisted state proves this: serializeArtifactPanelTabs drops exactly such tabs. The backfill effect in App.tsx waits for sourceRecordIds that never arrive on this block. In TurnCallsPanel, the selector falls back to a synthetic block:<turnId> entry next to the provisional prompt:<id> one, and the history effect is gated on the recordId/promptId props. The provisional navigation turn already knows blockId → promptId (from recordPromptAdmitted), so the panel can adopt it.

Fix (12 lines) + 2 tests: RED on head, GREEN with the fix
--- a/packages/web-shell/client/components/artifacts/TurnCallsPanel.tsx
+++ b/packages/web-shell/client/components/artifacts/TurnCallsPanel.tsx
@@ -492,6 +492,18 @@ export function TurnCallsPanel({
   const idle = usePromptStatus() === 'idle';
   const navigation = useTurnNavigationState();
   const blocks = useTranscriptBlocks();
+  // The sender's own live user block never carries a record or prompt id: its
+  // daemon echo is suppressed. Adopt the prompt id from the provisional turn
+  // bound to this block so the tab can persist, resolve and dedupe.
+  const provisionalPromptId =
+    !recordId && !promptId
+      ? navigation.provisionalTurns.find((turn) => turn.blockId === turnId)
+          ?.promptId
+      : undefined;
+  useEffect(() => {
+    if (provisionalPromptId)
+      onSelectPrompt?.(turnId, undefined, provisionalPromptId, promptLabel);
+  }, [provisionalPromptId, onSelectPrompt, turnId, promptLabel]);

The tests are appended to TurnCallsPanel.test.tsx. One checks that a sender-local block with only a provisional turn adopts prompt-live. The other checks that a tab which already has a promptId is not retargeted. On head the first fails, and with the fix both pass. TurnCallsPanel + App + loadTurnCalls tests: 1104/1104. ESLint, Prettier and web-shell tsc --noEmit are clean. The full patch is fix-sender-identity.patch.

Non-blocking notes

  1. The wrapped MCP call (tool_search → tool_call): the row name resolves to mcp__inventory__lookup_sku, but the description line reads tool_call and Arguments show the {name, arguments} envelope. The design doc says wrappers resolve their actual name and arguments. See the right pane of figure 3.
  2. English count grammar: 1 tool calls.
  3. The tooltip's "Start time" is when scheduling started, so it includes the approval wait (design-stated). On an approved call it reads earlier than the command actually began. A wording hint could help.
  4. Test coverage of session-tool-calls.ts: 18 targeted mutants run against its own test file, 11 killed. The one real gap is "never finalize dangling calls of a settled turn". The unit tests don't pin it, but the real-daemon crash case above shows the behaviour is correct. The other survivors are defense in depth: the reader already rejects bad anchors (400 on the real daemon), ownership closes at the next navigation record anyway, and the pagination loop ends through another bound.
  5. The triage bot left two questions for a maintainer. The Test result is now green, and server.test.ts passes locally as a full file. For the unvirtualized selector question, I measured a real long session: on a 4,227-turn session, opening the selector issues 17 turn-index page reads and renders all 4,227 options: about 18.7k DOM nodes and a ~104 MB page heap, usable after ~1.3 s. So it is reachable and still tolerable at that size, but it grows linearly. The 25k ceiling would be about 6× this.

Not covered: Windows/macOS, the agent detail tab contents, MCP auth, and an embedded host with showToolCalls=false restoring a persisted tab (triage finding 4).

Evidence (harness, raw ground-truth diffs, logs, figures): wenshao/qwen-code@823ef47/pr-12466

中文版

维护者验证:真实 daemon、真实工具执行、Linux(d10c768)

结论:建议补上一处客户端小修复后合并,或合并后立即跟进。 新的服务端读取接口端到端正确。在真实 qwen serve daemon 上,217 次真实工具调用的读取结果与原始 JSONL 逐条一致,daemon 重启后也一样。记录的耗时与墙钟时间吻合。面板在生产构建的 Web Shell 里工作正常。我发现了一处缺陷,出在用户最常走的路径上:发送 prompt 的那个客户端。在自己刚发的消息上打开“工具调用”时,tab 没有持久身份。该 prompt 在选择器里出现两次;tab 不会被持久化,刷新页面后面板关闭;该轮结束后也不会读取历史。下文给出根因、12 行修复和 2 个测试,修复已在同一套浏览器环境里验证。

测试方式(环境)
  • Linux x86_64,Node 22.22.2。对 head d10c768 与 base c83265ff36 分别执行 pnpm install --frozen-lockfile、完整 npm run build 与 npm run bundle。
  • 真实 qwen serve daemon 提供它自己打包的 Web Shell(生产 dist/web-shell,不是 vite dev)。浏览器为 Playwright 的 Chromium 1228。没有 page.route,没有 mock daemon。
  • 脚本化的 OpenAI 兼容模型发出真实工具调用,由 daemon 实际执行:shell、读/编辑/写文件、grep/glob、失败的读取、非零退出码、10 个一批的并行调用、子代理、重复的 provider id。两个真实 stdio MCP 服务器:一个是延迟加载的(经 tool_search → tool_call 调用),一个配置了 alwaysLoadTools。
  • 审批走真实的权限请求,daemon 在等待 1.5 秒后投票。取消通过 POST /session/:id/cancel。崩溃场景是在调用执行中对 daemon 发送 kill -9。
  • 生成数据和浏览之间重启了 daemon,因此读取走的是从磁盘冷启动的路径。

已通过实际执行验证

方面 证据 结果
完整性与归属 9 轮、217 次调用,与原始 chats/<id>.jsonl 比对。包括一轮 90 次调用的,和一轮被 loop cap 截停的 110 次调用(即“57 次只显示 13 次”那一类)。 每轮的数量、顺序、id 完全一致。缺失 0,多余 0,重复 0。daemon 重启后结果相同。sessions/live-state 始终为 [],说明读取不会挂载会话。
记录的耗时 假模型记录每次响应的发送时间和下一次请求的到达时间。 206 次带计时的调用,每一次的 [startedAt, startedAt+durationMs] 都落在它的真实墙钟区间内,且与原始 ui_telemetry 的 started_at/duration_ms 一致(206/206)。tooltip 精确到毫秒(24ms:19:59:19.428 → .452)。耗时包含审批等待(2 秒命令 + 1.5 秒审批 = 3548ms),与设计文档所述一致。
旧记录 由 base daemon 录制的会话,用 head 读取。 11 个耗时,0 个 startedAt。没有 tooltip,也没有 aria-description,没有伪造任何数据。
状态 工具报错、非零退出、重复 provider id、loop cap 拒绝、1.5 秒后取消、执行中 kill -9 daemon。 都显示为失败,且原因正确。取消显示 2s 已取消:记录的取消状态覆盖了通用的失败回放。崩溃并重启后,悬空调用被收尾为失败:“Tool result missing from saved history…”。
审批后的 ACP 帧 抓取 SSE。 tool_call_update(in_progress) 在 permission_resolved 之后 3ms 到达,带有 rawInput 与 startedAt。
运行中的 prompt 从输入框发出的一次真实 8 秒 shell 调用。 计时从 948ms 走到 8s,运行期间 /tool-calls 读取为 0 次。+5 秒时注入的 mid-turn 消息没有把这一轮切开:两次调用仍归在该 prompt 下,选择器里也没有为注入消息单列条目。
读取次数 page.on('request')。 选中一个 prompt = 1 次 GET;Refresh = 1 次;刷新页面恢复 = 1 次。另一轮运行时查看旧 prompt = 1 次,在 4 秒的流式输出期间没有重复读取。切回运行中的 prompt:运行期间 0 次,结束时 1 次,列表不闪空。
界面 中英文,浅色/深色主题。 工具名已本地化。MCP 标签和筛选正常(总数保持 11)。Shell 的“参数”是命令、“结果”是输出、“其他”默认折叠。编辑显示记录的 diff。JSON 已格式化。
接口约定 curl。 不带 token 或 token 错误都返回 401。turnId 缺失、空白、201 字符、重复传参、未知 uuid、非 prompt 记录、其他会话的记录,都返回 400 invalid_turn_anchor。通过另一个已注册的工作区读取该会话返回 404(不能跨工作区读取)。base 上该路由为 404。
测试(Linux) 本地 vitest。 core 586、acp-bridge 136、cli 1231、server.test.ts 整文件首次运行 1322/1322(作者在 macOS 上该文件不稳定)、sdk 878、web-shell 1470(10 个相关文件),全部通过。CI 全绿,包括 Test。

(图见上方英文部分)

缺陷:从自己发出的消息打开的 tab 没有持久身份

复现: 在输入框发送一个 prompt,然后在这条消息上点击查看工具调用(运行中或结束后都一样)。

PR head 应用下方修复后
该 prompt 在选择器中 出现两次 一次
localStorage 中的 turn_calls tab 无(被 serializeArtifactPanelTabs 丢弃) {promptId}
结束后的 /tool-calls 读取 0 次 1 次
刷新页面后的面板 关闭 恢复,且选中同一 prompt

在 head 上,同一会话的另一个旁观客户端表现正常(它从 bridge echo 拿到了 promptId)。所以问题只出在发送端,而这恰恰是最常见的情况。

根因。 发送端自己的用户回显被压制了(suppressOwnUserEcho)。正如 DaemonSessionProvider.tsx:362-364 的注释所说,这个本地块永远拿不到 recordId;App.openTurnCalls 据此创建的 tab 既没有 recordId 也没有 promptId。持久化状态可以证明这一点:serializeArtifactPanelTabs 丢弃的恰好就是这种 tab。App.tsx 里的回填 effect 在等 sourceRecordIds,而这个块永远不会有。在 TurnCallsPanel 中,选择器退回到一个合成的 block:<turnId> 条目,与临时条目 prompt:<id> 并列显示;历史读取的 effect 又以 props 上的 recordId/promptId 为前提。导航状态里的临时轮次已经知道 blockId → promptId(来自 recordPromptAdmitted),面板可以直接采用。

修复:TurnCallsPanel.tsx 增加 12 行(diff 见英文部分),并在 TurnCallsPanel.test.tsx 追加 2 个测试。一个验证只有临时轮次的发送端本地块会采用 prompt-live;另一个验证已有 promptId 的 tab 不会被重定向。第一个测试在 head 上失败,修复后两个都通过。TurnCallsPanel + App + loadTurnCalls 测试 1104/1104。ESLint、Prettier、web-shell 的 tsc --noEmit 均无问题。

非阻塞问题

  1. 经包装的 MCP 调用(tool_search → tool_call):行名称已解析为 mcp__inventory__lookup_sku,但描述行显示 tool_call,“参数”里是 {name, arguments} 外层信封。设计文档写的是包装调用会解析出实际名称和参数(见图 3 右栏)。
  2. 英文计数的单复数:显示为 1 tool calls。
  3. tooltip 的“开始时间”是调度开始的时间,包含审批等待(设计文档已说明)。对需要审批的调用,它会早于命令实际开始执行的时间,可以考虑在文案上提示。
  4. session-tool-calls.ts 的测试覆盖:用它自己的测试文件跑 18 个定向变异,杀掉 11 个。唯一真实的缺口是“已结束轮次的悬空调用从不收尾”:单测没有钉住,但上面真实 daemon 的崩溃场景证明行为是正确的。其余存活的变异属于纵深防御:reader 本身会拒绝错误锚点(真实 daemon 上返回 400);归属在下一个导航记录处本来就会关闭;分页循环也会被另一个边界终止。
  5. Triage 机器人给维护者留了两个问题。Test 现在已经绿了,server.test.ts 在本地整文件通过。关于选择器不做虚拟化的问题,我在真实长会话上测了:在 4,227 轮的会话上,打开选择器会发出 17 次 turn-index 分页读取,并渲染全部 4,227 个选项:页面约 1.87 万个 DOM 节点、JS 堆约 104 MB,约 1.3 秒后可用。所以这个规模是可达的,目前仍可接受,但开销线性增长,到 25k 上限时约为现在的 6 倍。

未覆盖: Windows/macOS、子代理详情页内容、MCP 认证,以及 showToolCalls=false 的嵌入宿主恢复已持久化 tab 的情况(triage 的第 4 条)。

证据(harness、原始对账数据、日志、图片):wenshao/qwen-code@823ef47/pr-12466


🤖 Generated with Claude Code — Claude Opus 5 (1M context)

@ytahdn

ytahdn commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

Review follow-up

Fixes pushed in 0ad95657ea and 6f68391f00. Verified the findings against d10c76801c and reproduced the functional defects before fixing them. Duplicate review comments are grouped below.

Findings Disposition
R1-1 Fixed the GET-only SSH allowlist. The request continues to use local persisted workspace/session storage; write methods remain rejected.
R1-2 Preserve failed agent diagnostic content even when a structured task result is present. Successful summaries remain compact.
R1-3 Wrapper-generated titles no longer hide the resolved tool description. Custom titles remain intact.
R1-5, R1-8 JSON strings remain Markdown JSON, as requested, but retain their exact number lexemes and duplicate keys. No parse/stringify rewriting.
R1-6 Any admitted running/queued prompt uses live data, including an earlier queued prompt.
R1-7, R1-53, R1-54 Mark goal-runtime replay messages as injected; share the panel/entry anchor predicate, suppress invalid goal/empty-cron entries, and test actual entry rendering/clicks and source updates.
R1-9, R1-36, R1-42–45 Replace the selector’s full backward scan with the shared navigation store and a keyboard-accessible window of at most 12 options. Visible pages load on demand, prompt switches retain the cache, and index errors/loading are shown without confusing unrelated history-navigation errors. Durable-ID recovery remains a separate bounded lookup when no record identity is available.
R1-10 Keep the running estimate within the browser clock domain. Recorded terminal duration/start/end remain authoritative. Reconnection can undercount the running estimate until completion; this is documented.
R1-11, R1-15 Timing accepts the raw wrapper or valid resolved tool name; unrelated names and subagent collisions remain rejected. Normalize whitespace consistently.
R1-47 Include the durable record ID in saved-row ownership checks.
R1-48 Keep retained live-only children beside their recorded parent, preserving each call exactly once.
R1-51, R1-52 Memoize expanded detail computation, skip unused result serialization for diffs, and bound rendered diffs with an explicit truncation notice.
R1-12, R1-58 Synchronize the bilingual design files list and remove the obsolete timing comment.
R1-33 Stop persisting prompt text in dock state, ignore legacy stored labels, and rebuild labels from the index; verified with a failing-before/passing-after restoration test.
R1-14, R1-46 Partially addressed and left open: older-daemon behavior is documented, but dedicated capability/protocol-reference work remains; the duplicate selector scan is gone, but missing-record-ID recovery can still repeat its bounded lookup.

R1-4 is not established as a regression of this PR. The actual Linux test job for the reviewed SHA passed (360 Web Shell files / 9,536 tests, no unhandled exception in that log). A strict local TrajectoryPanel run also passed 21/21. An isolated probe confirms an existing virtual-core debounce cleanup gap, but that dependency, virtualizer and triggering test are unchanged from the base. No unhandled-error suppression was added; the dependency cleanup issue should be tracked separately.

R1-25’s proposed early stop is not safe: records after the navigation ownership boundary can contain late results/timing for calls that started before it. The replay must continue to the next ordinary prompt boundary, within the existing explicit scan limits.

Fixed discussions are replied to and resolved; partially addressed and deferred discussions remain open.

Remaining suggestions are deferred to avoid further broadening this already extensively reviewed feature: nested-emitter timing coverage/wiring (R1-16/18), SDK cancellation/timeout options (R1-28), additional coverage-only cases (R1-13/17/19–24/26/27/29–32/38–41/50/56), and cleanup/polish (R1-34/35/37/49/55/57). In particular, the file action remains available on collapsed rows as explicitly requested; per-file availability-check coalescing is a follow-up. Fresh real-daemon verification of the originally reported large sessions is still pending, not claimed here.

Validation this round: 1,663 scoped unit tests passed across the most recent focused runs (Web Shell/App 1,491; ACP replay 138; persisted-reader/SSH tests 34). Independent reproductions retain failing snapshots of the original head and passing probes of the fixes. Full build/typecheck/bundle passed during the fixes; final Web Shell build/typecheck and changed-file ESLint/Prettier checks passed. Chromium mock-daemon E2E passed in light/dark themes (2/2); screenshots were visually checked and updated on the separate assets branch. These screenshots do not claim real daemon execution or Git backend validation. Updated E2E report. The final storage-only follow-up also passed its red/green App regression, Web Shell build/typecheck and commit hooks.

中文:已修复确认的功能、计时、数据保真、归属和性能问题,并补充回归验证。未证实的 CI 归因没有用忽略异常绕过;必须读取的跨边界迟到结果仍然保留。其余覆盖率、重构、嵌套计时扩展与界面微调建议按上表留作后续,避免继续扩大本 PR。截图使用模拟 daemon,真实大会话的新一轮执行验证仍未宣称完成。

@ytahdn ytahdn left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Ran an AI-assisted review over the diff and manually verified each finding against the code at this commit. Three issues worth addressing before merge — one functional bug, one behavioural defect in the ACP frame emission, and one paging-size mismatch that will desync the turn-index consumers. Details inline.

Comment thread packages/web-shell/client/components/artifacts/TurnCallsPanel.tsx
Comment thread packages/cli/src/acp-integration/session/Session.ts Outdated
Comment thread packages/web-shell/client/components/artifacts/TurnCallsPanel.tsx
@wenshao

wenshao commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification, round 2: real daemon, Linux (6f68391)

Follow-up to my round 1 report at d10c768. Only the delta is covered here.

Verdict: the same as round 1. I'd merge after one small client fix, or with it as an immediate follow-up. The follow-up commits hold up against a real daemon. The server read is byte-identical to round 1 across 4,262 turns. The new prompt selector cuts the cost on a 4,227-turn session from 18.5k DOM nodes / 104 MB to 1.6k / 38 MB. Persisted dock state no longer carries prompt text. But the round-1 finding is only half fixed. The duplicate selector entry is gone, because entries are now keyed by ordinal. When the sender opens View tool calls on their own message, the tab still has no durable identity. It is never persisted, so a reload closes the panel. No history read happens after the turn settles. The selector shows no check mark. That finding wasn't in the author follow-up table (that table maps /review findings), so it may simply have been missed. Below is an 18-line fix against the new code, with three tests. I verified it in the same browser rig.

How I tested (environment)
  • Linux x86_64, Node 22. The dependency lockfile is unchanged since d10c768, so I reused round 1's installed node_modules via hardlinks. Full npm run build and npm run bundle of 6f68391: exit 0.
  • Real qwen serve daemons serving their own bundled Web Shell (the production dist/web-shell), Chromium via Playwright, no page.route, no mock daemon. I used the same persisted sessions as round 1: the scripted OpenAI-compatible model and real tool execution (shell, file tools, two real stdio MCP servers, subagents, parallel batches of 10, loop cap, cancel, kill -9), plus the seeded 4,227-turn session.
  • A/B arms: d10c768 (round 1 head), 6f68391 (this head), and 6f68391 plus the patch below (only the SPA rebuilt).

What I checked

Area Method Result
Server read unchanged GET /tool-calls for every turn of 5 sessions, d10c768 vs 6f68391 daemons on the same data. 4,262 turns / 5,432 events byte-identical. Ground truth re-run on 6f68391: every turn equals the raw JSONL (125/125, 241/241, 20/20, 12/12 calls; order equal, 0 missing / extra / dup). All 362 timed calls fall inside their fake-model wall-clock window and equal ui_telemetry. The legacy base-recorded session still has 0 fabricated startedAt.
Unit tests (changed files) vitest. web-shell 1,377/1,377 (TurnCallsPanel, App, MessageItem, buildTrajectory, transcriptToMessages); cli 34/34 (session-tool-calls, ssh-workspace); acp-bridge 138/138 (transcript-replay).
Selector on 4,227 turns (R1-9/36/42–45) Open the selector, then Home, mid-list scroll, and choose. See the table below. On-demand paging works: scrolling to turn ~2,000 loaded only pages start=1800 and start=2000, rendered in 63 ms. Choosing by mouse at #2005 and by Home→Enter at #1 both retarget the panel and issue one /tool-calls read (200).
Prompt text out of dock state (R1-33) localStorage after opening a tab. Tabs carry turnId / recordId / promptId only: promptLabel is absent.
Wrapped MCP row (R1-3; my round-1 note 1) Expand mcp__inventory__lookup_sku reached via tool_search → tool_call. Fixed: the description no longer reads tool_call. Still open: Arguments show the {name, arguments} envelope (fig. 3).
Failed agent diagnostics (R1-2) New scenario: a foreground subagent whose model call returns HTTP 400. See note 1 below.

sender tab A/B

The remaining defect: the sender's tab has no identity

Repro: send a prompt from the composer, then click View tool calls on that message, either while it runs or after it finishes.

PR head 6f68391 With the patch below
Opened while running (SCN:slow, 9 s) not persisted, no check mark persisted {promptId}, checked
After the turn settles 0 /tool-calls reads (live rows only) 1 read (200)
Page reload panel closed panel restored, 2 rows
Opened shortly after sending (SCN:after) not persisted, 0 reads, closed on reload persisted {recordId}, 1 read, restored

Why round 1's root cause still applies: the sender's own user echo is suppressed, so that local block never gets sourceRecordIds or promptId. App.openTurnCalls therefore creates a tab with neither id. serializeArtifactPanelTabs drops it, the history effect is gated on selectedRecordId || promptId, and TurnCallPromptSelect receives recordId/promptId both undefined, so it never matches its selected row. The navigation store already knows this block's identity. While the turn runs, it is provisionalTurns[].blockId → promptId. After the turn settles, it is a live locations entry, blockId → turnId. The fix adopts that identity through the existing onSelectPrompt:

   const selectedPromptId = promptId ?? user?.promptId ?? indexedTurn?.promptId;
+  // The sender's own live user block never carries a record or prompt id: its
+  // daemon echo is suppressed. Adopt the identity navigation already tracks
+  // for that block so the tab can persist and read history once settled.
+  const adoptedPromptId =
+    recordId || promptId
+      ? undefined
+      : navigation.provisionalTurns.find((turn) => turn.blockId === turnId)
+          ?.promptId;
+  const adoptedRecordId =
+    recordId || promptId || adoptedPromptId
+      ? undefined
+      : [...navigation.locations.values()].find(
+          (location) => location.view === 'live' && location.blockId === turnId,
+        )?.turnId;
+  useEffect(() => {
+    if (adoptedPromptId || adoptedRecordId)
+      onSelectPrompt?.(turnId, adoptedRecordId, adoptedPromptId, promptLabel);
+  }, [adoptedPromptId, adoptedRecordId, onSelectPrompt, turnId, promptLabel]);

There are three tests: the running block adopts promptId, the settled block adopts recordId, and a tab that already has an identity is not retargeted. The test mock also gains locations. On 6f68391 the first two fail. With the fix all pass. TurnCallsPanel + App: 1,107/1,107. ESLint --max-warnings 0, Prettier, and web-shell tsc --noEmit are clean. Full patch: fix-sender-identity-r2.patch.

Selector on the real 4,227-turn session

d10c768 6f68391
Options in the DOM on open 4,227 10 (window of ≤ 12, aria-setsize=4227)
/turn-index requests on open 17 0
Page DOM nodes / JS heap 18,536 / 104 MB 1,642 / 38 MB
Open → stable 1,304 ms 306 ms
Home → first prompt rendered (all options already in the DOM) 20 ms

selector

Non-blocking notes

  1. R1-2 doesn't reach the real failure path. A real foreground subagent failure (the model returns 400) is recorded as a successful tool result carrying resultDisplay.status: "failed" and terminateReason: "Failed to run subagent: 400 …". On the wire it is tool_call_update with status: "completed", so the new status === 'completed' guard still strips its content. Both heads return identical bytes here. No diagnostic is lost: terminateReason survives in rawOutput. But the panel shows a green Completed with no reason (fig. 3). The chat transcript doesn't flag it either, so this is pre-existing ACP status behaviour. The new panel makes it look more authoritative, though. Reading rawOutput.status === 'failed' for task_execution rows would fix the badge.
  2. The selector re-requests a page while it is still in flight. In 2/2 runs, the mid-list scroll fetched start=2000 twice. missingPages goes from "1800,2000" to "2000" when the first page lands, the effect reruns, and loadOrdinal doesn't dedupe in-flight loads.
  3. Keyboard focus doesn't follow wheel scrolling. After scrolling the list with the mouse, aria-activedescendant is null and PageDown→Enter re-selects the old off-screen item (Upgrade @agentclientprotocol/sdk from 0.14.1 to 0.21.0 to unlock 3 session lifecycle methods (resumeSession / closeSession / unstable_forkSession) #4227). Clicking works.
  4. Still open from round 1 (the author deferred these as polish): the wrapped-MCP Arguments envelope and 1 tool calls.

row notes

Not covered: the SSH-workspace GET allowlist (R1-1) and goal-runtime replay marking (R1-7) were checked by unit tests only, and so was the diff truncation (R1-51/52). Windows/macOS were not covered.

Evidence (harness, per-turn diffs, API dumps, images): wenshao/qwen-code@d7b8cc9/pr-12466-round2

中文版

维护者验证第二轮:真实 daemon,Linux(6f68391)

这是对第一轮报告(d10c768,评论)的跟进,只覆盖增量部分。

结论与第一轮相同:建议补上一个客户端小修复后合并,或者合并后立即跟进这个修复。 在真实 daemon 上,这次的跟进提交都站得住。服务端读取结果在 4,262 轮上与第一轮逐字节一致。新的提示词选择器在 4,227 轮会话上把开销从 18.5k 个 DOM 节点、104 MB 降到 1.6k 个、38 MB。持久化的面板状态里也不再保存提示词原文。但第一轮报告的问题只修了一半。选择器里的重复条目已经没有了,因为条目现在按序号作键。发送端在自己的消息上打开"查看工具调用"时,这个 tab 仍然没有持久身份。它不会被持久化,刷新页面后面板就关闭了。这一轮结束后也不会读取历史,选择器里当前项也没有对勾。作者的跟进表对应的是 /review 的编号,没有包含这一条,可能只是漏看了。下面针对新代码给出一个 18 行的修复和 3 个测试,已在同一套浏览器环境里验证。

测试环境
  • Linux x86_64,Node 22。d10c768 之后依赖锁文件没有变化,所以用硬链接复用了第一轮安装好的 node_modules。对 6f68391 完整执行 npm run build 和 npm run bundle,均为 exit 0。
  • 真实 qwen serve daemon 提供自身打包的 Web Shell(生产版 dist/web-shell),浏览器是 Playwright 驱动的 Chromium。没有 page.route,也没有 mock daemon。 沿用第一轮持久化的会话:脚本化的 OpenAI 兼容模型驱动真实工具执行(shell、文件工具、两个真实 stdio MCP 服务器、子代理、10 个一批的并行调用、循环上限、取消、kill -9),另有预置的 4,227 轮会话。
  • 对照组:d10c768(第一轮 head)、6f68391(当前 head),以及 6f68391 加下方补丁(只重建了 SPA)。

检查项

方面 方法 结果
服务端读取不变 在同一份数据上,分别用 d10c768 和 6f68391 的 daemon 对 5 个会话的每一轮调用 GET /tool-calls。 4,262 轮、5,432 个事件逐字节一致。 在 6f68391 上重跑真值比对,每一轮都与原始 JSONL 一致(125/125、241/241、20/20、12/12 次调用;顺序一致,缺失、多余、重复均为 0)。362 个带计时的调用都落在假模型记录的真实时间窗口内,并且与 ui_telemetry 相等。base 录制的旧会话仍然没有伪造任何 startedAt。
单测(改动文件) vitest。 web-shell 1,377/1,377;cli 34/34;acp-bridge 138/138。
4,227 轮上的选择器(R1-9/36/42–45) 打开选择器,再测试 Home 键、滚动到列表中部和选择操作。 见下表。按需分页有效:滚到第 ~2,000 轮时只加载了 start=1800 和 start=2000 两页,63 ms 渲染完成。用鼠标选 #2005、用 Home→Enter 选 #1,面板都会切换过去,并各读取一次 /tool-calls(200)。
面板状态不存提示词原文(R1-33) 打开 tab 后检查 localStorage。 tab 只保存 turnId、recordId、promptId,没有 promptLabel。
包装的 MCP 行(R1-3,即第一轮非阻塞项 1) 展开经 tool_search → tool_call 调用的 mcp__inventory__lookup_sku。 描述行不再显示 tool_call,已修复。"参数"里仍是 {name, arguments} 外层信封(图 3),尚未解决。
失败 agent 的诊断信息(R1-2) 新增场景:一个前台子代理,它的模型调用返回 HTTP 400。 见下方非阻塞项 1。

遗留缺陷:发送端 tab 没有身份

复现: 在输入框发送一条提示词,然后在这条消息上点"查看工具调用"。运行中或结束后点都会出现问题。

PR head 6f68391 加补丁后
运行中打开(SCN:slow,9 秒) 未持久化,没有对勾 持久化了 {promptId},有对勾
本轮结束后 读取 /tool-calls 0 次(只有实时行) 读取 1 次(200)
刷新页面 面板关闭 面板恢复,显示 2 行
发送约 1 秒后打开(SCN:after) 未持久化,读取 0 次,刷新后关闭 持久化了 {recordId},读取 1 次,刷新后恢复

根因与第一轮相同: 发送端自己的用户回显被压制,这个本地块永远拿不到 sourceRecordIds 或 promptId。因此 App.openTurnCalls 创建的 tab 两个 id 都没有。结果是 serializeArtifactPanelTabs 把它丢弃,历史读取的 effect 因为 selectedRecordId || promptId 都为空而不执行,TurnCallPromptSelect 收到的 recordId 和 promptId 都是 undefined,所以匹配不到当前选中项。其实导航 store 已经知道这个块的身份。运行中时,可以从 provisionalTurns[].blockId 找到 promptId。结束后,可以从 live 的 locations 条目里由 blockId 找到 turnId。补丁通过现有的 onSelectPrompt 把这个身份写回 tab,代码见英文部分。补丁附 3 个测试:运行中的块能拿到 promptId、已结束的块能拿到 recordId、已有身份的 tab 不会被改写。测试 mock 也补上了 locations。在 6f68391 上前两个测试失败,加修复后全部通过。TurnCallsPanel + App 共 1,107/1,107。 ESLint、Prettier、web-shell tsc --noEmit 均无报错。

4,227 轮会话上的选择器

d10c768 6f68391
打开时 DOM 中的选项数 4,227 10(窗口最多 12 个,aria-setsize=4227)
打开时的 /turn-index 请求数 17 0
页面 DOM 节点数 / JS 堆 18,536 / 104 MB 1,642 / 38 MB
从打开到稳定 1,304 ms 306 ms
按 Home 到第一条提示词渲染出来 (所有选项已在 DOM 中) 20 ms

非阻塞项

  1. R1-2 的修复碰不到真实的失败路径。 真实的前台子代理失败(模型返回 400)被记录成一次成功的工具结果,其中带着 resultDisplay.status: "failed" 和 terminateReason: "Failed to run subagent: 400 …"。线上的 tool_call_update 是 status: "completed",所以新加的 status === 'completed' 判断仍然会清空它的 content。两个 head 在这里返回的字节完全相同。诊断信息并没有丢,terminateReason 还保留在 rawOutput 里。但面板显示的是绿色 Completed,也不显示失败原因(图 3)。聊天记录里同样没有标出失败,所以这是 ACP 状态层早已存在的行为,只是新面板让它看起来更权威。对 task_execution 行读取 rawOutput.status === 'failed',就能把这个徽标改对。
  2. 选择器会对还在请求中的页面重复请求。两次运行里,滚到列表中部时 start=2000 都被请求了两次。原因是第一页返回后 missingPages 从 "1800,2000" 变成 "2000",effect 重新执行,而 loadOrdinal 不会对进行中的请求去重。
  3. 键盘焦点不跟随滚轮滚动。用鼠标滚动列表后,aria-activedescendant 为 null,按 PageDown→Enter 选中的仍是屏幕外的旧项(Upgrade @agentclientprotocol/sdk from 0.14.1 to 0.21.0 to unlock 3 session lifecycle methods (resumeSession / closeSession / unstable_forkSession) #4227)。鼠标点击则没有问题。
  4. 第一轮遗留、作者作为细节打磨延后处理的两项:包装 MCP 行"参数"里的外层信封,以及英文单复数 1 tool calls。

未覆盖: SSH 工作区的 GET 放行(R1-1)、goal-runtime 回放标记(R1-7)、diff 截断(R1-51/52)只由单测覆盖。Windows 和 macOS 没有测试。

证据(harness、逐轮比对、API 转储、图片):wenshao/qwen-code@d7b8cc9/pr-12466-round2


🤖 Generated with Claude Code — Claude Opus 5.5 (1M context)

@ytahdn

ytahdn commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

Sender identity and new inline review follow-up

Fixed in 5aad27e6db. The sender-path defect was missed in the previous follow-up. A locally sent user message has no echoed promptId/record UUID, so checking only the transcript block was insufficient. The panel now adopts the identity already tracked by navigation: the admitted prompt while running, or the live record after settlement. This uses the existing selection callback so the dock persists the identity and the picker/history reader receive the same selection. Existing identities and foreign-session tabs are not retargeted.

The three new inline comments were also addressed:

  • Losing the workspace client clears the pending loading flag, so Refresh is not stranded disabled; stale completion remains ignored.
  • Approved starts go through the shared emitter. Prepared calls get updates; unprepared calls get creating frames with title/kind/locations/provenance/server metadata. Delivery failures remain non-fatal and the post-notification cancellation check remains. The permission request already carries the call ID, so the claim that the client could never have seen the ID was too broad; the metadata and consistent-emitter issue was valid.
  • Prompt-ID recovery uses the shared 200-entry index page size, including backward offsets and the short final page.

Regression checks failed before the fixes. Sender App tests cover both running and settled opening, stable identity persistence, checked selection, one history read at settlement, and restored content after reload. Panel tests also cover retained live rows, identity/owner guards, loss of client, and index page boundaries. Session tests exercise prepared/unprepared approved MCP metadata before execution and preserve notification-failure execution behavior.

Browser E2E

Verified the actual sender path in Chromium against the locally built real daemon and bundled Web Shell, with a local scripted OpenAI-compatible model and real shell/glob execution in an isolated temporary workspace. No intercepted browser responses or mock daemon. Running: persisted prompt ID, checked selector, no history polling. After settlement: one tool-calls request returning 200 and two rows. Reload: panel and both rows restored. Screenshots were visually checked. This does not claim a production-model run or repeat the maintainer's large-session survey.

Validation: 2,234 scoped tests passed (App 1,035; panel 77; Session 1,052; emitter 70); full build/typecheck/bundle, lint/format and commit hooks passed. Real-daemon browser report and screenshots.

Observed separately: a new empty session returned index 404s before prompt admission. The notice recovered after a successful index read; the light screenshot follows a manual index-only refresh while running, with history reads still zero. This initialization behavior is not claimed fixed. Before reload we also closed/reopened from the original sender message and verified record-ID persistence.

中文:上一轮漏掉了发送端回显被压制后的身份衔接,本次已从导航 store 补齐,并验证运行中/结束后打开、持久化、选中、结算读取和刷新恢复。新增三条评论均已修复:清除卡住的加载状态、统一审批后的起始通知、复用索引分页常量。浏览器使用真实 daemon、打包页面和真实工具执行,模型为本地脚本模型;没有用模拟 daemon 或拦截响应替代发送链路。维护者报告中的其他非阻塞建议不列为本次已修复。

@ytahdn

ytahdn commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

Merged origin/main b9840886b8 in 8fce4a3914 and resolved all nine conflicting files. Timing writers now use upstream startTime / started_at_ms; replay retains compatibility with earlier development records and preserves measured zero durations. Existing sender identity and panel restoration fixes remain intact.

Validation: 3,395 scoped tests passed; full build, typecheck and bundle passed; commit hooks passed. Two automatically merged stale timing assertions were corrected and the 51-test trajectory suite passed on rerun. Chromium against the real built daemon and a local scripted model verified running selection, one historical read on settlement, canonical recorded timing, reopening the original sender and restoring the panel after reload. Updated screenshots and bilingual evidence are in the PR summary. The initial empty-session index notice is documented as pre-existing, not fixed here.

已在 8fce4a3914 合并主分支并解决 9 处文件冲突。计时写入统一采用主分支字段,兼容旧开发记录并保留真实零耗时;发送端身份及面板恢复修复均保留。3,395 项相关单测、完整构建/类型检查/打包及提交钩子通过。真实 daemon 浏览器验证覆盖运行中选择、结算后历史读取、计时回放、原消息重新打开及刷新恢复;中英文截图证据已更新。

@ytahdn

ytahdn commented Sep 23, 2026

Copy link
Copy Markdown
Collaborator Author

Merged origin/main 40ef07ab35 in 20a5809053. Resolved the sole conflict in the trajectory panel by retaining the upstream overview implementation; the resolved component and its tests match main. GitHub now reports MERGEABLE.

Validation: 1,210 scoped Web Shell tests, full build/typecheck/bundle and commit hooks passed. Real-daemon Chromium verification passed for sender selection, timing replay, reopening and reload restoration, plus rendering/selecting the upstream tool timeline spans. Screenshots and bilingual evidence are updated in the summary. The browser harness needed one correction to reopen the right panel before the new overview check; no product change was needed.

已合并最新主分支,唯一冲突保留上游时间轴实现,GitHub 确认无冲突。1,210 项相关单测及完整构建/类型检查/打包通过;真实 daemon 浏览器验证覆盖工具面板恢复和时间轴点击,截图证据已更新。

@wenshao

wenshao commented Sep 23, 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.

@ytahdn

ytahdn commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator Author

Merged origin/main 906418aa9b in 2e424efa8e. Resolved the three conflicts by preserving upstream conversation search alongside the existing durable-record Tool calls mapping, keeping both sets of regression tests and the embedding options documentation.

Validation: 1,379 scoped Web Shell tests passed; full build, typecheck, bundle and commit hooks passed. Real-daemon Chromium verification passed for sender identity, timing replay, reopening, reload restoration and trajectory tool-span selection. Updated screenshots and bilingual evidence are in the summary. Search behavior is covered by unit tests, not claimed as browser-tested in this run.

已合并最新主分支并解决三个文件冲突,保留会话搜索、历史工具调用定位和双方测试。1,379 项相关测试、完整构建/类型检查/打包及提交钩子通过;真实 daemon 浏览器验证也通过,截图证据已更新。

wenshao added a commit to wenshao/qwen-code that referenced this pull request Sep 24, 2026
@wenshao

wenshao commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification, round 3: real daemon, macOS (2e424ef)

Follow-up to round 1 (d10c768) and round 2 (6f68391). This round covers what changed since 6f68391: the sender-identity fix (5aad27e), the three origin/main merges (8fce4a3, 20a5809, 2e424ef), and one shared-code change that came with the fix. Earlier rounds ran on Linux. This one runs on macOS.

Verdict: the round-2 blocker is fixed, and I'd merge. When the sender opens View tool calls on their own message, the tab now gets a durable identity. I tested three timings: right after sending, while the turn is running, and after it settles. In all three, the identity is persisted and the selector shows a check mark. Live updates cause 0 history reads, the settled turn causes exactly 1, and a page reload restores the panel. After the merges, recorded timing still matches the raw JSONL call-for-call. That holds for sessions recorded by this PR and by main, and for a legacy copy with no start timestamps (0 fabricated). Nothing below blocks the merge.

How I tested (environment)
  • macOS 26 (arm64), Node 24.18.1, pnpm tree from scripts/setup-worktree.js. There are two isolated worktrees: PR head 2e424ef and its merge base 906418a, which is the current origin/main. On both, npm run build && npm run bundle exited 0.
  • Real qwen serve daemons (dist/cli.js) serve their own bundled Web Shell. Chromium is driven by Playwright. No page.route and no mock daemon. The model is a local scripted OpenAI-compatible server, and tools really run: shell (sequential, parallel, failing), glob, and approval prompts answered through the real permission endpoint. Each arm has its own QWEN_HOME/QWEN_RUNTIME_DIR.
  • Arms: 2e424ef daemon; 906418a (main) daemon; and a third 2e424ef daemon pointed at the main-recorded data directory. The third arm tests cross-version reads.
  • Harness: wenshao/qwen-code@109dbe6/pr12466/r3/rig.

What I checked

Area Method Result
Sender identity (R2 blocker) Send from the composer, then open View tool calls on that message at three timings: at ~0 ms (instant), at 1.5 s while running, and after the turn settles. Record localStorage, the selector, the /tool-calls requests, and a page reload. Fixed in all three. instant persists {promptId}. At 1.5 s and after settling, the tab persists {recordId}. The selector check mark is correct. There are 0 reads while live and exactly 1 read (200) at settlement. After reload, the panel is restored with 2 rows (fig. 1).
Mutation check on the fix Six single-point mutations of the adoption code in TurnCallsPanel.tsx, each run against TurnCallsPanel.test.tsx + App.test.tsx (1,148 tests). 5/6 killed. Removing the effect: 4 tests fail. Dropping promptId adoption: 2. Dropping recordId adoption: 2. Dropping the owner guard: 1. Retargeting an existing identity: 1. Survivor: removing location.view === 'live' (note 3).
Timing after the merges Ground truth: for every turn, compare GET …/tool-calls with the raw JSONL. That covers the call IDs between turn anchors and the ui_telemetry started_at_ms/duration_ms. PR-recorded sessions: 14 turns, 23 calls. Order matches in 14/14 turns, with 0 missing and 0 extra. All 22 timed calls match exactly. Main-recorded sessions, read by the PR daemon: 16 turns, 26 calls, order 16/16, timing 26/26. This includes a copy with every started_at_ms stripped: 0 fabricated startedAt.
Approved-call start frame (new shared behaviour in 5aad27e) Wire A/B test. A default-mode session gets one shell call that needs approval, answered through POST /session/:id/permission/:requestId. I recorded the SSE stream on the main daemon and on the PR daemon. See the table below. The PR adds one tool_call_update in_progress after approval and no second creating tool_call, so no duplicate card.
Duplicate prompt text One session has two identical SCN:parallel prompts. Open the panel from the last one. The panel checks option #5 (not #0) and reads that prompt's own turnId. The tooltip times belong to that prompt (fig. 2).
Search coexistence (merge 2e424ef) Diff the resolved TranscriptViewport.tsx against main, then in the browser: Search this conversation → SCN:slow → Enter → View tool calls on the hit. Compared with main, the resolution adds only the TurnCallsProvider wrapper and its opener. Main's MessageList props, including the historical overrides, are identical after whitespace normalisation. In the browser, the panel opens the searched prompt (20f3425a) with its 2 rows (fig. 2).
Empty-session index notice (documented by the author) Open the panel on a brand-new session 0 ms and 300 ms after sending. Not reproduced in 2/2 attempts. Every turn-index request returned 200 and no alert was rendered.
Unit tests / static Every PR-changed test file, per package, plus typecheck, ESLint and Prettier. web-shell 1,553/1,553, acp-bridge 144/144, core 588/588, sdk 879/879. cli serve + acp-integration: 3,998/3,999; the one failure (acp-output.test.ts "ACP EOF output") fails identically on main. ssh-workspace.test.ts: the same 12 local-environment Git failures on main and on the PR; the PR's tool-calls allowlist case passes. Typecheck exits 0 for all 5 packages. eslint --max-warnings 0 and prettier --check on changed files: clean. CI on 2e424ef is green.

sender identity

Approved-call wire A/B

5aad27e removes the didRequestPermission gate. Every non-todo call now goes through emitStart after approval, so the change reaches every ACP client, not just the Web Shell. What each daemon sent for the same call:

Vote main 906418a PR 2e424ef
Allow once tool_call pending → permission → tool_call_update completed tool_call pending → permission → tool_call_update in_progress (kind execute, full title, startedAt) → tool_call_update completed (+ startedAt, durationMs)
Reject tool_call pending → permission → tool_call_update failed same sequence, plus startedAt and durationMs: 1513 on the failed frame

The streamed call was prepared, so the start goes out as an update, not as a second creating frame. The Web Shell renders a single card. ChannelBase consumers now see an in_progress transition after approval. Before, they saw only pending → completed. That is a real status change. I think it's correct, and I'm flagging it only because it affects the channels as well. I did not exercise the unprepared-call path (a provider that doesn't stream tool calls).

history, timing, search

Non-blocking notes

  1. A rejected call shows its approval wait as elapsed time. The row reads Cancelled · 2s, and its tooltip gives start/end times for a command that never ran (fig. 2, last panel). The data is honest: the JSONL ui_telemetry record already stores this duration on main (1,523 ms for the same rejection on the main arm; 1,513 ms on the PR arm, equal to its live frame), and the PR's risk section documents that approval wait is part of the scope. Hiding the elapsed badge for rejected or never-executed calls would avoid implying that the command ran for 2 s.
  2. Carried over from round 2, unchanged: TurnCallPromptSelect.tsx and the server read path are byte-identical to 6f68391. So the in-flight page re-fetch, the missing focus follow after wheel scrolling, the green Completed badge on a failed foreground subagent, and the wrapped-MCP {name, arguments} envelope all still apply. So does 1 tool calls: the panel header still says it, while the message area already says "1 tool call" (visible in both figures).
  3. Small test gap: no test fails when location.view === 'live' is removed from the recordId adoption. A test with a historical location sharing the same blockId would pin it.

Not covered this round: Windows and Linux (rounds 1–2 were Linux), a production model, SSH workspaces, and the large-session selector survey (unchanged since round 2).

中文版

维护者验证第三轮:真实 daemon,macOS(2e424ef)

这是第一轮(d10c768)和第二轮(6f68391)的跟进,只覆盖 6f68391 之后的变化:发送端身份修复(5aad27e)、三次合并 origin/main(8fce4a3、20a5809、2e424ef),以及随修复一起进来的一处共享代码改动。前两轮在 Linux 上测,这一轮在 macOS 上测。

结论:第二轮的阻塞问题已修复,我建议合并。 发送端在自己的消息上点「查看工具调用」时,tab 现在有了持久身份。我测了三个时机:刚发送、运行中、结束后。三种情况下身份都会持久化,选择器也都有对勾。运行中读取历史 0 次,结束时恰好 1 次,刷新页面后面板恢复。合并之后,记录的计时仍与原始 JSONL 逐条一致。这对本 PR 录制的会话、main 录制的会话,以及一份去掉了开始时间戳的旧格式副本都成立(伪造的时间戳为 0 个)。下面各项都不阻塞合并。

测试环境
  • macOS 26(arm64),Node 24.18.1,pnpm 依赖树(scripts/setup-worktree.js)。两个隔离 worktree:PR head 2e424ef 和它的合并基点 906418a(即当前 origin/main)。两边 npm run build && npm run bundle 均 exit 0。
  • 真实 qwen serve daemon(dist/cli.js)提供它自己打包的 Web Shell,浏览器是 Playwright 驱动的 Chromium。没有 page.route,也没有 mock daemon。 模型是本地脚本化的 OpenAI 兼容服务,工具真实执行:shell(串行、并行、失败)、glob,以及通过真实权限接口回应的审批。每个臂有独立的 QWEN_HOME/QWEN_RUNTIME_DIR。
  • 三个臂:2e424ef 的 daemon;906418a(main)的 daemon;第三个是 2e424ef 的 daemon 指向 main 录制的数据目录,用来测跨版本读取。
  • 装置脚本:wenshao/qwen-code@109dbe6/pr12466/r3/rig。

检查项

方面 方法 结果
发送端身份(第二轮阻塞项) 从输入框发送,然后在三个时机点这条消息的「查看工具调用」:约 0 ms(instant)、运行中 1.5 秒、结束后。记录 localStorage、选择器、/tool-calls 请求,以及刷新页面后的状态。 三种时机都已修复。 instant 持久化 {promptId};1.5 秒和结束后打开时持久化 {recordId}。选择器对勾正确。运行中读取 0 次,结束时恰好 1 次(200)。刷新后面板恢复,显示 2 行(图 1)。
修复的变异检验 对 TurnCallsPanel.tsx 的身份采纳代码做 6 个单点变异,每个都跑 TurnCallsPanel.test.tsx + App.test.tsx(1,148 个测试)。 6 个杀掉 5 个。 去掉 effect:4 个测试失败。去掉 promptId 采纳:2 个。去掉 recordId 采纳:2 个。去掉 owner 守卫:1 个。改写已有身份:1 个。存活的一个是去掉 location.view === 'live'(非阻塞项 3)。
合并后的计时 真值比对:逐轮比较 GET …/tool-calls 与原始 JSONL,包括轮次锚点之间的调用 ID,以及 ui_telemetry 的 started_at_ms/duration_ms。 PR 录制的会话: 14 轮、23 次调用,14/14 轮顺序一致,缺失 0、多余 0,22 个计时全部相等。main 录制、由 PR daemon 读取的会话: 16 轮、26 次调用,顺序 16/16,计时 26/26。其中包括一份删掉全部 started_at_ms 的副本:伪造的 startedAt 为 0 个。
审批后的起始帧(5aad27e 新增的共享行为) 线上 A/B:default 模式会话里发一个需要审批的 shell 调用,通过 POST /session/:id/permission/:requestId 投票,分别在 main 和 PR 的 daemon 上录制 SSE。 见下表。PR 在批准后多发一帧 tool_call_update in_progress,不会再发一个新建的 tool_call,所以不会出现重复卡片。
提示词文本重复 同一会话里有两条完全相同的 SCN:parallel,从最后一条打开面板。 选中的是第 5 项(不是第 0 项),读取用的是这一轮自己的 turnId,悬浮提示里的时间也属于这一轮(图 2)。
与会话搜索共存(合并 2e424ef) 把解完冲突的 TranscriptViewport.tsx 与 main 对比;再在浏览器里操作:「搜索此会话」→ SCN:slow → 回车 → 在命中消息上点「查看工具调用」。 与 main 相比,冲突解法只多了 TurnCallsProvider 包裹和打开回调;main 的 MessageList props(包括历史视图的覆盖项)在归一化空白后完全一致。浏览器里,面板打开的正是搜到的那一轮(20f3425a),显示它的 2 行(图 2)。
空会话索引提示(作者自述) 在全新会话里,发送后 0 ms 和 300 ms 各打开一次面板。 2/2 次都没有复现:所有 turn-index 请求都返回 200,也没有出现错误提示。
单测 / 静态检查 PR 改动的所有测试文件(按包跑),以及类型检查、ESLint、Prettier。 web-shell 1,553/1,553,acp-bridge 144/144,core 588/588,sdk 879/879。cli 的 serve + acp-integration:3,998/3,999,唯一失败的(acp-output.test.ts「ACP EOF output」)在 main 上同样失败。ssh-workspace.test.ts:main 和 PR 上是同样 12 个本机环境导致的 Git 失败,PR 新加的 tool-calls 放行用例通过。5 个包的类型检查都 exit 0。改动文件的 eslint --max-warnings 0 和 prettier --check 都通过。2e424ef 上的 CI 全绿。

审批路径的线上 A/B

5aad27e 去掉了 didRequestPermission 这个门,所有非 todo 调用在批准后都会走 emitStart。因此这个改动影响所有 ACP 客户端,不只是 Web Shell。同一个调用,两边 daemon 发出的帧如下:

投票 main 906418a PR 2e424ef
允许一次 tool_call pending → 权限请求 → tool_call_update completed tool_call pending → 权限请求 → tool_call_update in_progress(kind execute、完整标题、startedAt)→ tool_call_update completed(带 startedAt、durationMs)
拒绝 tool_call pending → 权限请求 → tool_call_update failed 同样的序列,failed 帧多带 startedAt 和 durationMs: 1513

这个流式调用在批准前已经预先发过帧,所以起始帧以更新形式发出,而不是第二个新建帧;Web Shell 只渲染一张卡片。ChannelBase 的消费方现在会在批准后看到一次 in_progress 状态变化,以前只有 pending → completed。这是真实的状态变化,我认为是对的,这里提出来只是因为它也影响渠道。没有预先发帧的路径(不流式输出工具调用的 provider)这一轮没有测。

非阻塞项

  1. 被拒绝的调用把审批等待时间显示为耗时。 这一行显示 已取消 · 2s,悬浮提示给出了开始/结束时间,但这个命令从来没有执行过(图 2 最后一栏)。数据本身没问题:main 的 JSONL ui_telemetry 记录里本来就有这个耗时(main 臂同样的拒绝是 1,523 ms;PR 臂是 1,513 ms,与实时帧相等),PR 的风险说明也写明了耗时包含审批等待。如果对被拒绝或从未执行的调用隐藏耗时标记,就不会让人以为命令跑了 2 秒。
  2. 第二轮遗留,没有变化: TurnCallPromptSelect.tsx 和服务端读取路径与 6f68391 逐字节相同。因此以下几项仍然存在:进行中的分页被重复请求、滚轮滚动后键盘焦点不跟随、前台子代理失败时仍显示绿色「已完成」、包装的 MCP 行「参数」里仍是 {name, arguments} 外层信封。英文单复数问题也还在:面板标题仍写 1 tool calls,而消息区已经写的是「1 tool call」(两张图里都能看到)。
  3. 小的测试缺口: 从 recordId 采纳逻辑里去掉 location.view === 'live' 后,没有任何测试失败。补一个「历史位置与 live 位置 blockId 相同」的用例就能钉住它。

本轮未覆盖: Windows 和 Linux(前两轮是 Linux)、生产模型、SSH 工作区,以及大会话选择器的测量(自第二轮以来代码没有变化)。

@ytahdn

ytahdn commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator Author

@qwen-code /triage

@qqqys qqqys 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.

Independent Critical-only review — head 2e424efa

Not approving, on budget and on one unresolved Critical rather than on a new finding. This is a 7,706-line, 64-file change whose first review round filed 21 Criticals, and I could not independently verify the fixes or scan the feature surface inside the budget.

The one Critical still open: R1-4, disputed and not closed by either side

R1-4 claims the PR reproducibly makes npm test --workspace=packages/web-shell exit non-zero on Linux — base c83265ff exits 0 — via a single unhandled ReferenceError: window is not defined from a @tanstack/virtual-core debounce setTimeout, which vitest attributes to the untouched TrajectoryPanel.test.tsx.

The thread is still unresolved and the two sides have not converged:

  • The reporter disclosed that the exact culprit file could not be isolated — no single file and no candidate+victim pair under --no-file-parallelism reproduces it; it needs the full-suite worker context. The anchor on App.test.tsx was named as the most plausible leak source, not a demonstrated one.
  • The author's rebuttal is that the Linux job for the reviewed SHA passed (360 files / 9,536 tests), a strict local TrajectoryPanel run passed 21/21, an isolated probe confirms a pre-existing virtual-core debounce cleanup gap, and the dependency, both virtualizers (MessageList.tsx, TrajectoryPanel.tsx) and the cited victim test are all unchanged from base, with no exception suppression added. It explicitly leaves the finding open pending a reproducible failing full-suite environment or an isolated culprit.

What I can state from my own read at this head: Test (ubuntu-latest, Node 22.x) is completed/success, as are web-shell E2E Smoke (ubuntu-latest, Node 22.x), Lint & Static, Integration Tests (no-AK, No Sandbox) and Real daemon E2E / Java 11, with Test (macos) and Test (windows) skipped rather than failing. So the symptom as stated — a red web-shell suite on Linux CI — is not present at 2e424efa, and the mechanism cited lives in code this diff does not touch.

I am nonetheless not treating R1-4 as confirmed fixed. Nothing was changed to close it, the reporter has not retracted, and I did not run the suite myself to test the local-reproduction claim. Under a Critical-only standard, an unresolved and genuinely disputed blocker that I cannot confirm either way is not something I can approve past.

Twenty resolved Criticals I did not verify in code

Round 1's other Criticals are all on resolved threads with concrete fix replies pinned to 6f68391f00 — the SSH-workspace GET boundary now admitting the new route as a GET-only passthrough with write methods still rejected; wrapper-generated titles resolving to the real tool name instead of the literal tool_call; live elapsed estimates using the browser receipt clock only rather than subtracting a server-clock timestamp from a client-clock Date.now(); and timing-pairing guards accepting either the recorded wrapper name or the resolved tool name while retaining the identity and collision checks.

Those replies are specific and plausible, and the green Linux test job is real evidence for the parts that were test-visible. But a resolved flag plus an author reply is not verification, and I read none of them against the code at head. I am reporting them as unconfirmed rather than as fixed.

Surfaces I did not scan

I performed no independent Critical-only pass over the feature this PR adds. Unread at head: the new daemon tier — packages/cli/src/serve/session-tool-calls.ts and the GET /workspaces/:workspace/session/:id/tool-calls route, including its ownership classification, the 100-page × 250-record scan budget, the two 32 MiB byte budgets, the tool_calls_replay_incomplete code, and its archive-coordination and runtime-resolution wrapper; the SSH-workspace passthrough change itself, which is a trust-boundary widening and deserves a read of its own rather than acceptance on a reply; transcriptToMessages.ts and buildTrajectory.ts wrapper-name resolution; and TurnCallsPanel clock handling. Given that round 1 found 21 Criticals across exactly these areas, I would not want to approve them on someone else's audit.

Also outstanding and disclosed by the author: R1-13, a Suggestion noting that fresh real-daemon verification of the original 57-call/139-call sessions and a many-call server fixture are still open, and that the current browser evidence uses a mock daemon so it does not establish live persistence correctness. I did not gate on it — it is a Suggestion, and the author left it open rather than overstating the evidence, which is the right call — but it means the endpoint's live behaviour is unverified by anyone so far.

CI

Green at head as itemised above; no failure attributable to this diff. CI is not the basis for this verdict, and R1-4 turns on a local full-suite reproduction that CI does not settle either way.

Verdict: COMMENT — One Critical (R1-4) is unresolved and disputed: the claimed red Linux web-shell suite is not reproduced by CI at this head and the cited mechanism sits in unchanged code, but nothing was changed to close it and I could not confirm it either way. The remaining gate is independent verification, at head, of the twenty resolved Criticals and of the new daemon route tier — its ownership scope, scan and byte budgets, and the SSH passthrough widening. Next step: re-request review at this head so a fresh pass can read those surfaces, and for R1-4 either supply the reproducible full-suite environment the reporter asked for or record the pre-existing virtual-core cleanup gap as a separate issue so this thread can be closed on evidence rather than left contested.

@ytahdn

ytahdn commented Sep 24, 2026

Copy link
Copy Markdown
Collaborator Author

@qqqys Follow-up to review 5301167259, at head 2e424efa8ee875995edf0c363db85a262c0f3e76:

R1-4: The current Linux test job, Web Shell E2E Smoke, and Lint & Static pass. Passing CI does not rule out an intermittent timer leak, but no PR-specific cause has been isolated and the reported failure is not demonstrated by CI at this head. I have replied on the original thread requesting closure as an unconfirmed PR regression, rather than retaining it as a Critical blocker. This is not a claim that the dependency cleanup gap was fixed; a reproducible current-head environment and complete logs would justify reopening it.

Correction to the verification summary: The statement that current browser evidence uses only a mock daemon is outdated. Subsequent runs, including this exact head, used the built real local daemon and bundled Web Shell in Chromium, with a local scripted OpenAI-compatible model and actual shell/glob execution, without browser response interception. They verified persistence of the sender prompt identity while running, no tool-history polling during execution, a historical read on settlement, recorded tool timing, reopening from the original sender message, and restoration after reload. The current run also verified trajectory tool-span selection. Bilingual report and limitations, restored panel screenshot. These are real-daemon persistence checks, not production-model or large-session validation.

Still outstanding: Fresh verification of the original 57-call/139-call turns and the proposed many-call, multi-page server-reader fixture remain outstanding. The two-call browser scenario does not replace them. Independent verification of the twenty resolved findings and the daemon route/SSH ownership boundaries also remains useful; a resolved thread or author reply is not a substitute for that review. Your review explicitly did not perform that pass, so I understand this as remaining review work rather than a new demonstrated defect.

Please update the assessment to reflect the newer real-daemon evidence and reconsider R1-4's blocker classification on the current-head evidence. We are not asking that missing verification be treated as completed, or that an approval be based solely on green CI.


中文补充:R1-4 已在原讨论附上当前提交的绿色 CI,并请求按“尚未证实为本 PR 回归”关闭阻塞项,不声称依赖清理缺口已修复。整体 review 中“目前只有 mock daemon 验证”的信息已过时:当前提交已通过真实本地 daemon、实际 shell/glob 执行的发送端身份、历史计时、重新打开及刷新恢复验证,证据见上方报告。原始 57/139 次调用轮次、多调用跨页服务端用例,以及已修复项和接口归属边界的独立复核仍明确保留为待完成项,不以两次调用的浏览器场景替代。请据此更新验证描述并重新考虑 R1-4 的阻塞级别。

@chiga0 chiga0 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.

Scope: Standard tier — new API endpoint, SDK surface expansion, React UI panel, core pipeline fixes. Reviewed: session-tool-calls.ts, the new route in session.ts, loadTurnCalls.ts, turnCallsContext.tsx, TurnCallsPanel.tsx (first half + key state logic), App.tsx, TranscriptViewport.tsx, ArtifactPanel.tsx, MessageItem.tsx/MessageTimestamp.tsx, DaemonClient.ts, types.ts, ui/types.ts, normalizer.ts, transcript.ts, coreToolScheduler.ts, session-transcript-reader.ts, transcript-replay.ts, history-replayer.ts, history-replay-page.ts, toolClassification.ts, transcriptToMessages.ts, buildTrajectory.ts. Not reviewed: TurnCallsPanel.test.tsx (2472 lines, test validity only), TurnCallPromptSelect.tsx (prompt selector UI), SSH-workspace route passthrough in full context, all docs.

No blocking findings.


Non-blocking observations

Session.ts — didRequestPermission guard removed (Minor)
emitStart is now emitted for all non-todo tools, including permission-requested non-agent tools. Previously those tools did not receive a tool.start frame at this call site. Behaviorally this changes when the tool block first appears in the transcript (before vs. after approval). upsertToolBlock handles a duplicate start idempotently via existingId lookup, so this does not corrupt state, but callers relying on "no block before approval" would see a change. Looks intentional given the panel needs in-flight calls to appear immediately.

session-transcript-reader.ts — ??= change (confirm R1-36 as correct)
currentPromptTurn.promptId ??= entry.turnResultPromptId correctly prevents turnResultPromptId from overwriting a daemonPromptId-set value. The new daemonPromptId field is now assigned to turn.promptId first; the ??= ensures the first writer wins rather than the last. No concern.

buildTrajectory.ts + transcript-replay.ts — goal_runtime/goal_control as injected sources
Both INJECTED_USER_SOURCES and isTurnCallsPrompt now exclude goal_runtime/goal_control. The old comment called this a "known gap — costs a spurious turn header." The PR closes it consistently server-side and client-side.

transcriptToMessages.ts — tool_call wrapper title resolution
title: block.title === block.toolName ? toolName : block.title — when the title was auto-generated from the raw tool name, it upgrades to the resolved inner name. When the title was set to something other than the tool name, it's preserved. Correct.

TurnCallsPanel — promptId/promptLabel not forwarded from TranscriptViewport
openViewportTurnCalls has signature (turnId: string) => void and calls openTurnCalls(turnId, recordId) without promptId or promptLabel. App.tsx's openTurnCalls handles this gracefully: it falls back to user?.promptId and user.text from the store. UX-only: the label won't be available until the store lookup resolves.


Cross-check against existing reviews

R1-4/R1-5 (web-shell suite non-zero on Linux): CI passes at 2e424efa on Ubuntu Node 22 (360 files / 9,536 tests). The cited mechanism — @tanstack/virtual-core debounce setTimeout — is in code this diff does not touch. I did not run the suite locally. Cannot confirm or refute the local-reproduction claim; recording as unverified. The CI green is real evidence, the local claim is unresolved.

R1-6 (title stays tool_call for wrapper calls): Fixed by the block.title === block.toolName ? toolName : block.title guard. Confirmed at head.

R1-20/R1-21 (timing-pairing guards): Both buildTrajectory.ts and transcript-replay.ts guards now accept timing.toolName matching either the raw tool name or the resolved inner name (&& logic, correct). Confirmed.

R1-35 (scan continues reading pages after selectionClosed): Records past selectionClosed are intentionally retained — the replay comment says "late interleaved results" need the broader span for result pairing. The byte budget (2 × SESSION_TRANSCRIPT_MAX_EXPANDED_PAGE_BYTES) caps the exposure. I read this as a design choice, not a stop-condition bug.

R1-36 (??= precedence): Confirmed as correct (see note above).

R1-2 / SSH-workspace GET passthrough (not in my read scope): Not independently verified. Existing review thread shows a specific fix was applied at 6f68391f; CI green is supporting evidence but not a substitute for reading that change.


No blockers. No approvalBlockers. Approving.

Reviewed with AI assistance.

@qqqys qqqys 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.

Correction to my review of head 2e424efa — verdict unchanged (COMMENT)

The head has not moved since my review, so this is not a re-review. It corrects one statement in it that is wrong, records a reclassification I now accept, and restates why the verdict does not change.

I was wrong about the live-daemon evidence

I wrote that current browser evidence uses a mock daemon and drew the conclusion that "the endpoint's live behaviour is unverified by anyone so far." That conclusion is false. I sourced it from the author's earlier disclosure on the R1-13 thread and presented it as the state at this head without checking whether it still held. It does not: runs at this exact head used the built real local daemon and the bundled Web Shell in Chromium, with a local scripted OpenAI-compatible model and actual shell/glob execution and no browser response interception, and they verified sender prompt identity persisting while running, no tool-history polling during execution, a historical read on settlement, recorded tool timing, reopening from the original sender message, restoration after reload, and trajectory tool-span selection.

The author states the limits of that evidence themselves — it is real-daemon persistence checking, not production-model or large-session validation — which is the right framing, and it is narrower than the many-call cases still outstanding. But "real-daemon verified for these behaviours" and "unverified by anyone" are different claims, and mine was the wrong one.

R1-4: I accept the reclassification, and it does not by itself change the verdict

The author asks that R1-4 be closed as an unconfirmed PR regression rather than retained as a Critical blocker, and is explicit that this is not a claim the virtual-core cleanup gap was fixed — a reproducible current-head environment with complete logs would justify reopening it.

That position is consistent with what I found independently: Test (ubuntu-latest, Node 22.x), web-shell E2E Smoke and Lint & Static are green at this head; the reporter disclosed that no single file and no candidate/victim pair under --no-file-parallelism reproduces the failure; and the dependency, both virtualizers and the cited victim test are unchanged from base. I called it "unresolved and disputed" and said I could not confirm it either way. I still cannot confirm a PR-caused regression, and on that evidence I no longer treat it as a blocker I am holding this PR against.

Two things stay true and are worth keeping separate: the thread is still unresolved and the reporter has not retracted, so this is a reclassification on the available evidence rather than a demonstrated closure; and the underlying timer-cleanup gap in the dependency is real and unfixed, which belongs in its own issue rather than in this PR's verdict.

Why the verdict is still COMMENT

Setting R1-4 aside entirely, the gates I named remain open, and the author agrees they do:

  • The twenty Criticals from round 1 sit on resolved threads with specific fix replies pinned to 6f68391f00, but I read none of them against the code at head. A resolved flag plus an author reply is not verification.
  • I performed no independent Critical-only pass over the new daemon tier — session-tool-calls.ts and GET /workspaces/:workspace/session/:id/tool-calls, its ownership scope, the 100-page × 250-record scan budget, the two 32 MiB byte budgets, tool_calls_replay_incomplete, and the archive-coordination and runtime-resolution wrapper.
  • The SSH-workspace passthrough change is a trust-boundary widening that deserves its own read rather than acceptance on a reply.
  • Fresh verification of the original 57-call/139-call turns and a many-call, multi-page server-reader fixture is still outstanding, as the author states; the two-call browser scenario does not substitute for it.

The author's framing is the same as mine: this is remaining review work, not a newly demonstrated defect, and neither green CI nor a missing pass should be treated as a completed one.

Verdict: COMMENT (unchanged) — I approve of nothing being merged on the strength of this note; it corrects my record and narrows the open gates from three to two. What stands between this PR and an approval from me is an independent read, at head, of the twenty resolved fixes and of the new daemon route tier including the SSH passthrough boundary. Re-request review when that pass can be funded, or point a reviewer at those two surfaces directly.

@wenshao
wenshao dismissed a stale review September 24, 2026 08:19

fixed

@wenshao
wenshao added this pull request to the merge queue Sep 24, 2026
Merged via the queue into QwenLM:main with commit 8fb6ade Sep 24, 2026
159 of 160 checks passed
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