Skip to content

fix(mcp): Support larger Apps, scoped tool calls and isolated origins - #12258

Merged
samuelhsin merged 32 commits into
mainfrom
codex/amplitude-mcp-app-limits
Sep 27, 2026
Merged

samuelhsin merged 32 commits into
mainfrom
codex/amplitude-mcp-app-limits

Conversation

@samuelhsin

@samuelhsin samuelhsin commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

Important

Remote HTTPS renderer verified at d6d532eb8b: d6d532eb8b's unmodified production App component and sandbox handler were deployed in an isolated fixture on the actual remote instance, reached through its existing authenticated HTTPS gateway and port proxy. The public Tableau Superstore chart rendered, West filtering worked, and page reload followed by filtering passed. Report and screenshots. Authentication/tool results were mocked; the existing remote Qwen service was not upgraded. Full remote daemon/session integration and authenticated Tableau Cloud acceptance remain unverified. Subsequent history-binding, failed-App cleanup, reconnection, deadline and permission-budget fixes are covered by focused tests; the remote fixture has not been rerun for these follow-ups.

远端 HTTPS 渲染已在 d6d532eb8b 验证: 已将 d6d532eb8b 的原始 App 组件与沙箱部署到真实远端实例的隔离夹具,通过现有 HTTPS 网关和端口转发完成 Tableau Public 图表显示、West 筛选、页面重载及再次筛选。认证和工具结果为 mock,原有远端 Qwen 服务未升级;完整 daemon/会话整合与 Tableau Cloud 认证仍未验收。 后续历史绑定、失败清理、重连、超时和审批预算修正通过定向测试验证,本次未重跑远端夹具。此前 4 MiB 测试属于独立的本地主端口转发回归,不能与本轮约 335 KB 的远端样例混为一谈。

What this PR does

Repairs three independent MCP App integration failures: bounded per-server resource loading, App-initiated server tools, and opaque iframe origins. In the verified local-browser/local-daemon setup, the official Tableau App renders an authenticated Cloud chart inside Qwen Code, supports Region filtering, and renders again after a full Qwen page reload. The production renderer and sandbox at d6d532eb8b also passed the remote HTTPS fixture described above; full remote daemon/session integration remains unverified.

The existing 1 MiB / 10-second resource defaults remain. Explicit settings are bounded at 4 MiB / 120 seconds, propagate through SDK initialization and daemon configuration, and use separate pooled tools for different policies. When optional HTML exceeds live-delivery or history-page budgets, the completed text result and navigation survive; healthy delivery and reconnect replay retain the original HTML.

Apps can call App-visible tools on their bound server through the owning session's existing permission, hook and cancellation pipeline. Paging to earlier turns preserves that App session binding while historical source and feedback controls remain disabled. App-only tools stay out of model tool discovery. Raw App results, including embed tokens, return privately to the App; model/transcript output receives a fixed summary. App approval admission reserves model capacity within the existing session cap. App calls repair dead connections for later calls even with the daemon's invocation guard installed, without replaying the failed attempt. All App cards on one browser page share a two-request limit; additional requests wait client-side so approval/control requests can use HTTP/1.1 connections. Queued calls retain progress heartbeats and cancel without being submitted. App recovery uses the actual calling session's configuration and explicitly restarts its surviving pool entry, so a JSON-RPC session error cannot silently clear the tool/resource directory or reconnect another session's same-name server. App calls retain a separate five-minute bridge ceiling (including approval), with a 310-second SDK REST timeout; a longer MCP tool timeout does not extend that App ceiling. Attachment replacement and refreshed client IDs are handled, and background App calls no longer leave the composer stuck in Processing.

On HTTP loopback hosts, each render first receives a unique origin on a dedicated static-only loopback listener. Both iframe layers preserve origin identity, allowing a nested vendor iframe to use its actual origin. The App remains separated from Qwen's UI/API and other Apps. One-use, expiring registrations pin the CSP and parent origin; the listener has no daemon API or WebSocket endpoint and closes with its owner. HTTP CSP now enforces the sandbox independently of iframe attributes. Remote/HTTPS hosts use a small opaque data bootstrap through the existing daemon connection immediately; local isolated-listener failure switches to that path after 10 seconds. HTML travels over checked postMessage instead of a large navigation URL. The App stays opaque while vendor descendants retain their own origin. A failed data handshake or 30-second initialization timeout shows text fallback.

Why it's needed

Amplitude's real App resource exceeds both original resource limits. Tableau's 335,305-byte resource fits the defaults, but first requires an App-to-server embed-token call and then fails when the sandbox forces its nested view to Origin: null. Increasing HTML limits cannot solve those latter failures. The earlier failed Tableau report is superseded by the successful authenticated browser verification below.

Reviewer Test Plan

How to verify

  • Return 1,048,577 bytes of App HTML: defaults should retain the tool text and show an actionable size warning; a 2 MiB allowance should render the same App. An 11-second resource read should require an explicit App deadline above 10 seconds. Cancellation should preserve the completed tool text.
  • Queue a large App alongside progress frames and navigate recorded history: completed text and subsequent messages must remain usable when optional HTML is omitted; healthy replay must retain the original App. Both resource settings must survive stdio, HTTP and SSE configuration round trips.
  • Open an App in a bound session and approve its App-visible server tool. Confirm the result reaches the App without exposing its raw private result to model history or the transcript. Model-only, disabled, foreign-server and foreign-session calls must be rejected; cancel or disconnect must cancel the associated approval without cancelling an unrelated model approval.
  • Navigate to an older turn containing an App in the same bound session: App-visible tool calls should still work. Return to the latest turn and repeat. A standalone transcript or a mismatched tools session must remain display-only, and historical feedback controls must remain disabled.
  • Run the official Tableau local MCP server with MCP Apps enabled and valid Cloud OAuth. Render a view in the actual Qwen App card, change Region, and reload Qwen. Expect the chart and filter updates to render, and the composer to remain usable.
  • Confirm each render uses a fresh isolated origin; its consumed URL and policy overrides fail, and requests from that origin to Qwen's protected API remain rejected.

Evidence (Before & After)

Real Tableau, in Qwen Code: the official unmodified @tableau/mcp-server 4.8.1 App, authenticated Tableau Cloud and the Superstore sample workbook were used in the user's existing Chrome on macOS. Only model tool selection was made deterministic by a local model fixture; MCP calls, resource HTML, OAuth, embed-token retrieval and the chart were real. The fixture's chat sentence is not the rendering assertion; the visible chart and successful interaction are the evidence.

Before: Origin: null blocks the real view After: real view rendered inside Qwen
Before: Tableau origin-null failure After: Tableau rendered in Qwen

Region filter interaction: selecting West changed the map, KPIs and monthly charts inside the same Qwen App card.

West filtering works inside Qwen

Full Qwen page reload: the view rendered again under a fresh isolated origin, with the Qwen composer idle and usable.

Real Tableau view after Qwen reload

The successful local Qwen page was http://127.0.0.1:18897/session/06ca808a-1483-490a-b6de-9246a8344e53; this is a local validation URL, not a hosted demo. The local official MCP endpoint was http://127.0.0.1:18891/tableau-mcp. Its mcp-apps feature gate was enabled through the official custom-provider mechanism; neither server code nor App HTML was patched. The hosted https://mcp.tableau.com tool listing observed in this test did not advertise the App rendering/token tools, so the hosted endpoint is not claimed as an Apps pass.

The Tableau HTML measured 335,305 bytes and one authenticated resources/read took 254 ms. That measures the HTML resource read, not all JavaScript, chart data, map tiles or total render time. No complete dependency-transfer size is claimed.

Theme: Qwen already supplies hostContext.theme (dark/light) on initialization and updates it without remounting the App. The existing DOM regression covers theme switching. This Tableau App version does not apply that field to the embedded chart, so a white Tableau chart inside dark Qwen is expected in these screenshots; a dark Tableau chart was not verified.

Resource-limit fixtures and authenticated Amplitude observations

Baseline tests used the global CLI with a local MCP fixture: 1,048,577-byte HTML was rejected and an 11-second read was cancelled after about 10 seconds. The PR build rendered the fixture with an explicit allowance, including interaction and cold-daemon replay.

Default resource limit Configured allowance
Default limit warning Configured fixture rendered

Cold restart replay screenshot · History navigation screenshot · Reproduction fixtures. These are synthetic resource/history tests, not real Amplitude analytics.

On 2026-09-20, two OAuth-authenticated reads from https://mcp.amplitude.com/mcp returned the same 2,333,981-byte (2.23 MiB) App HTML in 88.715 s and 79.107 s. This version fits 4 MiB but exceeds 1 MiB, and both reads exceed 10 seconds. The documented example uses appResourceMaxBytes: 4194304 and appResourceTimeoutMs: 120000. These observations do not guarantee future size or latency. Replay through the actual core and bundled CLI preserved identical HTML and its persisted SHA-256 under the configured allowance.

The earlier real-HTML browser run with a synthetic chart ID reached the missing-host-tools error. This PR now adds App-to-server tools, but an authenticated Amplitude chart has not been rerun to completion; Tableau success must not be extrapolated to Amplitude.

Tested on

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

Current review follow-up validation

The latest follow-up addresses maintainer round4 F1/F2: a page-wide two-request App queue prevents a single-page App burst from occupying every HTTP/1.1 connection needed by approvals; App-only repair now works with the daemon guard while retaining initial execution authorization and never replaying the failed request. At 23c0e1bd90, real local WebKit/daemon tests passed for bursts of five and nine normally approved calls and MCP crash recovery. 232 focused tests and full build/typecheck/bundle passed; screenshots and details are in the latest PR reply. This is the report's bounded option (b), not asynchronous 202/event-stream completion; the queue is not shared across browser tabs and does not provide a universal connection reservation across arbitrary additional event streams.

The preceding follow-up (5f17519bf2) addressed pooled App recovery after request-level session errors. A source-integration reproduction showed a still-connected entry losing its App/resource registry; a second reproduction showed the initial fix reconnecting the pool bootstrap session instead of the caller. The final regression verifies caller-session binding, exact connection selection, restored tools/resources, no replay, and a successful explicit next call. New/replaced connections and ordinary refresh do not receive an extra restart; empty/failed restart results are rejected. At 5f17519bf2, 1,429 tests across five files, full build, typecheck, bundle, ESLint and Prettier passed. This run uses mocked SDK transport responses through the real pool/manager/registry chain, not a new remote browser or authenticated Tableau run.

The prior follow-up fixed R5-1 through R5-5: consistent remote sandbox documentation, no theme updates to a failed/closed App bridge, reconnect-without-replay for unguarded App calls, SDK/bridge timeout ordering, and reserved model approval capacity. Focused tests cover these regressions and the existing history/session boundaries. The existing remote screenshots remain tied to d6d532eb8b.

In the prior review round, full build, typecheck and bundle passed, along with 2,070 tests in seven focused files, ESLint and Prettier. Real App/AppBridge SDK probes passed 4/4: the old handler timed out at 60 seconds, while progress notifications kept a 95-second call alive; cancellation stopped all progress timers. Benign Chromium/WebKit embedding probes passed 4/4 across isolated and opaque fallback modes, including a simulated single-port forwarded connection. These use local fixtures, not authenticated Tableau. The Tableau screenshots below/above remain evidence from the earlier authenticated run, not a rerun of this head.

The review follow-up also preserves original HTML for an empty live queue, retains the newest replay App when evicting older text is sufficient, and preserves in-flight App calls when a reversible close/restore gate is released. Forced close and shutdown still cancel them. The protocol documents now describe per-subscriber lossy copies and their unchanged event IDs.

Independent maintainer verification: round4 reports Chromium/WebKit data-mode origin, forwarded-port and B1 isolation checks passing at 5f17519bf2. That is the maintainer's synthetic real-daemon/browser evidence, not a rerun of authenticated Tableau Cloud or our remote instance. Our earlier blocked security-probe limitation remains part of the historical record; this follow-up does not rerun attack probes.

Environment (optional)

macOS arm64, Node.js 24.19.0; locally built CLI and daemon-backed WebShell. Earlier authenticated Tableau validation (before this review follow-up): full build, typecheck and bundle; 607 passing tests across 14 focused Core, ACP bridge, CLI, SDK and WebShell files; ESLint and Prettier. Independent sandbox HTTP probes passed 12/12 and complete-daemon origin isolation probes passed 14/14. The latter rejected sandbox-origin API calls, API/WS paths on the static listener, policy overrides and consumed URLs. Client-ID regression failed before its fix and passed after it. Prior resource-limit verification included 1,424 targeted tests, SDK public-type checks, CLI configuration round trips, live delivery and historical navigation; these are earlier scoped runs, not an additional final-run total.

Risk & Scope

  • Larger accepted HTML increases transcript/replay size. The SDK reads the full resource before the byte check; this is not a streaming or peak-memory cap. Different policies use different pool entries and may require separate stdio processes.
  • This is a cross-package host/bridge change and changes the sandbox origin boundary, requiring maintainer review. Isolation is the unique App origin versus Qwen and other Apps; the inner App and its own proxy share an origin. CSP and ungranted sandbox capabilities remain enforced; merely adding allow-same-origin on Qwen's own origin would not be equivalent.
  • The data mode addresses the inaccessible extra-listener case while retaining an opaque App and real vendor descendant origins. Both the local single-port Docker/Chrome regression and the remote HTTPS Tableau Public fixture at d6d532eb8b passed. Full remote daemon/session integration and authenticated Tableau Cloud acceptance of this commit remain unverified. Stable _meta.ui.domain mapping is not implemented; an origin cannot be assigned by setting an arbitrary vendor-domain string.
  • Authenticated Amplitude chart rendering, complete dependency-download measurements and Tableau dark-chart styling remain unverified. App resource reads, tool listing and additional host capabilities are outside this change.
  • Existing resource defaults remain and no configuration migration is needed. All credentials remain in private local validation files and are excluded from the PR.

Design: resource policy (English) · 资源策略(中文) · App tools and origins (English) · App 工具与来源(中文).

Linked Issues

Refs #11945. Resource loading, scoped App server calls and the reproduced Tableau origin failure are addressed; full Amplitude chart rendering remains unverified.

中文说明

本 PR 的修改

修复三个独立的 MCP App 接入问题:按服务器配置有界资源加载、App 发起服务器工具调用,以及 iframe 不透明来源。在已验证的本地浏览器与本地 daemon 部署中,官方 Tableau App 已在 Qwen Code 内显示经过认证的 Cloud 图表,Region 筛选和 Qwen 整页重载均通过;远端 data 模式及单端口回归已完成,实际 HTTPS 部署仍待上方所述验收。

保留 1 MiB / 10 秒默认资源限制;显式配置上限为 4 MiB / 120 秒,通过 SDK 初始化及守护进程配置传递,不同策略使用不同连接池工具。可选 HTML 超过实时交付或历史页预算时保留工具文字及导航;正常交付和重连回放保留原始 HTML。

App 可通过所属会话现有的权限、hook 和取消流程调用绑定服务器上对 App 可见的工具。App 专用工具不进入模型工具发现。App 错误恢复绑定实际调用会话配置,并显式重启其仍存活的连接池条目,避免 JSON-RPC 会话错误清空工具/资源目录或误重连其它会话的同名服务器;失败调用不自动重放。包括嵌入令牌的原始结果仅返回 App,模型及会话记录只收到固定摘要。支持 attachment 替换和 clientId 更新;后台 App 调用不会再让输入框卡在 Processing。

HTTP loopback 宿主首先在独立、仅提供静态内容的监听器上为每次渲染取得唯一来源。两层 iframe 保留来源身份,使嵌套的第三方 iframe 能使用自身真实来源。App 与 Qwen 页面/API 及其它 App 隔离。一次性、会过期的注册固定 CSP 和父页面来源;监听器不提供守护进程 API 或 WebSocket,并随所属应用关闭。HTTP CSP 独立于 iframe 属性强制执行沙箱。远端/HTTPS 宿主直接经现有连接使用小型不透明 data 引导页,本地独立监听器 10 秒不可达时也切换到该路径。HTML 经严格校验的 postMessage 传输,避免大导航 URL。App 保持不透明,第三方后代保留自身来源;data 握手失败或 30 秒初始化超时后显示文字回退。

修改原因

Amplitude 的真实 App 资源超过原有两个限制。Tableau 的 335,305 字节资源低于默认上限,但先需要 App 调用嵌入令牌工具,随后又因沙箱使嵌套页面变为 Origin: null 而失败。增加 HTML 上限无法解决后两项问题。下列真实认证浏览器验证已取代此前失败记录。

Reviewer Test Plan

验证方式

  • 返回 1,048,577 字节 App HTML:默认配置保留工具文字并显示可操作警告;配置 2 MiB 后渲染相同 App。11 秒读取需要高于 10 秒的显式 App 时限,取消后保留已完成工具文字。
  • 大型 App 与进度事件共同入队、浏览历史记录:移除可选 HTML 时,已完成文字和后续消息仍可用;正常回放保留原始 App。两项限制经 stdio、HTTP、SSE 配置读写后保持不变。
  • 在绑定会话中打开 App 并批准其 App 可见工具:原始私有结果应仅返回 App,不进入模型历史或会话记录。拒绝仅模型可见、禁用、其它服务器或其它会话的调用;取消和断连只取消相关审批,不影响无关模型审批。
  • 使用启用 MCP Apps 的官方本地 Tableau 服务和有效 Cloud OAuth,在实际 Qwen App 卡片显示视图、切换 Region、重载 Qwen。图表与筛选结果应显示,输入框保持可用。
  • 每次渲染使用新来源;已使用 URL 及策略覆盖请求失败,从该来源访问受保护 Qwen API 仍被拒绝。

修复前后证据

真实 Tableau、真实 Qwen 页面: 在 macOS 用户现有 Chrome 中,使用未经修改的官方 @tableau/mcp-server 4.8.1 App、经过认证的 Tableau Cloud 和 Superstore 示例工作簿。仅模型工具选择采用确定性的本地 fixture;MCP 调用、HTML、OAuth、嵌入令牌和图表均真实。fixture 的聊天句子不作为成功断言,图表可见内容及交互结果才是证据。

上方前后对照截图展示 Origin: null 失败与 Qwen 内成功显示。West 筛选截图显示地图、指标和月度图同步更新;重载截图显示图表在新的隔离来源下重新渲染,Qwen 输入框保持空闲可用。

成功验证页面为 http://127.0.0.1:18897/session/06ca808a-1483-490a-b6de-9246a8344e53,这是本地验证地址,不是线上演示。本地官方 MCP 为 http://127.0.0.1:18891/tableau-mcp,通过官方 custom-provider 机制启用 mcp-apps,未修改服务代码或 App HTML。本次观察的 hosted https://mcp.tableau.com 工具列表未公开 App 渲染/令牌工具,因此不宣称 hosted 端点通过 Apps 验证。

Tableau HTML 为 335,305 字节,一次已认证的 resources/read 耗时 254 ms。这仅衡量 HTML 资源读取,不包含全部 JavaScript、图表数据、地图瓦片或总渲染时间,未宣称完整依赖下载大小。

主题: Qwen 已在初始化时传递 hostContext.theme(dark/light),切换时实时更新且不重新挂载 App,已有 DOM 回归覆盖。该版 Tableau App 未将此字段应用到嵌入图表,因此截图中的黑色 Qwen 内呈现白色 Tableau 图表;未验证 Tableau 深色图表。

资源限制 fixture 与真实 Amplitude 观察

基线用全局 CLI 和本地 MCP fixture,1,048,577 字节 HTML 被拒绝,11 秒读取在约 10 秒取消。PR 构建在显式提高上限后显示相同 fixture,并通过交互及守护进程冷重启回放。默认/配置后截图、冷重启截图、历史导航截图及复现 fixture 链接见上方,均为合成资源/历史测试,不是真实 Amplitude 分析数据。

2026-09-20,两次通过 OAuth 读取 https://mcp.amplitude.com/mcp 均取得 2,333,981 字节(2.23 MiB) 相同 HTML,分别耗时 88.715 秒、79.107 秒。该版本低于 4 MiB、高于 1 MiB,耗时均超过 10 秒。文档示例为 appResourceMaxBytes: 4194304 和 appResourceTimeoutMs: 120000。这些值不保证未来资源大小或延迟。经实际 core 和构建 CLI 回放,配置后 HTML 及持久化 SHA-256 保持一致。

此前真实 HTML 配合合成 chart ID 的浏览器测试显示缺少宿主工具能力,截图链接见上方。本 PR 现已补充 App 服务器工具调用,但尚未重新完成经过认证的 Amplitude 图表验证,不能从 Tableau 成功推断 Amplitude 成功。

测试平台

浏览器为 macOS 用户 Chrome;本轮另含 Linux Docker 内服务的单端口转发回归,Windows 未测试。

本轮审核修正验证

最新修正处理维护者第4轮 F1/F2:同页所有 App 共用两个在途请求的上限,避免一次 App 并发占满审批所需的 HTTP/1.1 连接;App 连接恢复允许 daemon guard 存在,但首次执行仍需授权,失败调用绝不重放。排队中的调用保留心跳并支持取消。23c0e1bd90 的真实本地 WebKit/daemon 验证通过 5/9 并发审批与 MCP 崩溃恢复;232 项定向测试及完整 build/typecheck/bundle 通过,截图与细节见最新 PR 回复。这采用报告中的最小方案 (b),尚未改为 202/事件流异步交付;队列不跨标签页共享,也不保证任意多个事件流下的连接容量。

前次 5f17519bf2 修正处理连接池中的 App 会话错误恢复:真实 pool/manager/registry 链路复现了目录丢失及误重连启动会话的问题。回归验证实际会话绑定、目标连接选择、工具/资源恢复、不重放,以及下一次显式调用成功;普通刷新、新建或替换连接不额外重启,失败或空重启结果不视为成功。5f17519bf2 的 5 个文件共 1,429 项测试、完整 build/typecheck/bundle、ESLint 和 Prettier 通过。本轮仅 mock SDK transport 响应,未重跑远端浏览器或 Tableau 认证图表。

此前审核轮次的完整 build、typecheck、bundle,以及 7 个定向文件共 2,070 项测试、ESLint 和 Prettier 通过。真实 App/AppBridge SDK 探测 4/4 通过:旧处理器在 60 秒超时,修正后进度通知使 95 秒调用成功,取消后所有进度计时器清除。Chromium/WebKit 正常嵌入探测 4/4 通过,覆盖独立来源、不透明回退及模拟仅转发主端口的连接。这些使用本地 fixture,不是真实认证 Tableau;上方 Tableau 截图来自此前认证实测,并非此版本重新运行。

本轮同时保留空实时队列的完整 HTML;当淘汰旧文字即可满足预算时保留最新回放 App;可恢复的关闭/恢复门闩释放时保留进行中的 App 调用,强制关闭和退出仍会取消。协议文档已说明各订阅者的有损副本及不变的事件 ID。

维护者独立验证: 第4轮报告 确认 5f17519bf2 的 Chromium/WebKit data 模式来源、端口转发及 B1 隔离检查通过。这是维护者的合成真实 daemon/浏览器证据,不是本轮重跑真实 Tableau Cloud 或我们的远端实例。本轮没有重跑攻击探测。

环境

macOS arm64、Node.js 24.19.0、本地构建 CLI 与守护进程 WebShell。此前真实 Tableau 验证(本轮审核修正前)包括完整 build、typecheck、bundle,以及 Core、ACP bridge、CLI、SDK、WebShell 共 14 个文件 607 项定向测试通过,以及 ESLint、Prettier。独立沙箱 HTTP 探测 12/12 通过,完整守护进程来源隔离探测 14/14 通过;后者验证 App 来源 API 请求、静态监听器 API/WS 路径、策略覆盖和已使用 URL 均被拒绝。clientId 回归修复前失败、修复后通过。此前资源限制验证包含 1,424 项定向测试、SDK 类型检查、CLI 配置读写、实时交付及历史导航;这些是之前按范围执行的验证,不计入本次测试总数。

风险与范围

  • 更大的 HTML 增加记录和回放负载。SDK 在字节检查前读取完整资源,该限制不是流式传输或峰值内存上限。不同策略使用不同连接池条目,可能启动不同 stdio 进程。
  • 本次涉及跨包宿主/桥接和沙箱来源边界,需要维护者审核。隔离边界是唯一 App 来源与 Qwen/其它 App;本地独立来源模式中 App 与自身代理同源;data 模式中 App 与代理隔离。CSP 和未授予的沙箱能力仍受限制,不能简单在 Qwen 自身来源上增加 allow-same-origin 来替代。
  • data 模式解决额外 listener 不可达的情况,同时保持 App 不透明和第三方后代的真实来源。本地单端口 Docker/Chrome fixture 及 d6d532eb8b 的真实远端 HTTPS Tableau Public fixture 已通过;后续提交未重跑远端,完整 daemon/会话整合与 Tableau Cloud 认证仍待验收。未实现稳定 _meta.ui.domain 映射;不能填写任意第三方域名就获得该来源。
  • 真实 Amplitude 图表、完整依赖下载量、Tableau 深色图表仍未验证。App 发起资源读取、工具列表和其它宿主能力不在本次范围。
  • 保留现有资源默认值,无需配置迁移。全部凭证留在本地私有验证文件,不进入 PR。

资源策略与 App 工具/来源的完整中英文设计文档链接见上方。

关联 issue

关联 #11945。已解决资源加载、受限的 App 服务器调用及复现的 Tableau 来源问题;完整 Amplitude 图表仍未验证。

@samuelhsin

Copy link
Copy Markdown
Collaborator Author

Local verification on e59d0f0a6e (macOS arm64, Node.js 24.19.0): PASS.

Scenario Observed result
Default, 4,096-byte HTML Full App HTML retained
Default, 1,048,577-byte HTML Size warning; successful tool output retained
Explicit 2 MiB, same 1,048,577-byte HTML Full App HTML retained
Default deadline, 11-second read, general MCP timeout 30 seconds App resource timeout at the default 10-second deadline
Explicit App deadline 15 seconds, 11-second read Success
Explicit App deadline 500 ms, 1.5-second read App resource timeout

The real daemon-backed WebShell rendered the configured larger resource inside its MCP App sandbox; clicking the fixture button changed it to “Interaction verified”. After stopping and restarting the daemon, the saved transcript retained exactly 1,048,577 HTML bytes, rendered the App, and supported the same interaction. Three unedited screenshots are embedded in the PR body; the fixture and screenshots contain only synthetic test data.

Full build + bundle, repository-wide typecheck, four focused suites (347 tests), ESLint, Prettier and diff checks passed. Tests also cover UTF-8/base64 limits, configured boundary/clamping, cancellation, pool isolation, metadata/session tool projections, and worst-case JSON escaping of the final 4 MiB ceiling. Independent review caught a replay-budget risk in the initial larger ceiling; the final ceiling leaves approximately 8 MiB of envelope headroom under the existing 32 MiB replay limit.

Limit of this evidence: the real Amplitude endpoint was contacted but returned 401 invalid_token; OAuth authorization did not complete. No real Amplitude chart or external Data Agent embedding was verified. Local fixture rendering must not be read as a claim of authenticated Amplitude compatibility.

中文

最终提交已在 macOS arm64 / Node.js 24.19.0 上验证。6 个本地 CLI E2E 场景全部通过:默认小资源成功、默认超大资源拒绝、配置后超大资源成功、默认 11 秒读取超时、配置 15 秒后慢读取成功、配置 500 ms 后及时超时。

真实守护进程/WebShell 沙箱成功渲染同一份 1,048,577 字节 HTML,按钮交互成功。停止并重启守护进程后,保存的记录仍保留完整 HTML,回放展示和交互再次通过。PR 正文附三张未修改的实际浏览器截图,均为合成测试数据。

完整构建及 bundle、全仓类型检查、347 项相关测试、ESLint、Prettier 和 diff 检查均通过。独立审查发现的序列化回放预算问题已修正:最终 HTML 上限为 4 MiB,即使最坏 JSON 转义也为现有 32 MiB 回放限制预留约 8 MiB 空间。

真实 Amplitude 端点返回 401 invalid_token,OAuth 未完成,因此未验证真实 Amplitude 图表或外部 Data Agent 嵌入。fixture 成功不代表真实服务兼容性已验证。

@samuelhsin
samuelhsin dismissed a stale review via 6353051 September 19, 2026 15:32
@samuelhsin

Copy link
Copy Markdown
Collaborator Author

Addressed the review's documentation finding: the pool-key comment now explicitly covers shared tool-snapshot settings and explains why App resource limits must remain in the fingerprint—they are stored during discovery and are not re-projected per session. This follow-up changes comments only.

The pool-fragmentation tradeoff is now recorded in the PR's English and Chinese risk sections. Different configured policies require separate entries for correct isolation; redesigning the per-name budget is outside this resource-loading fix. The request and abort signal intentionally share the same resolved deadline, as already documented and covered by the short-timeout case.

Validation: full repository build and typecheck, 27 pool-key tests, ESLint, Prettier, and a clean diff audit. Existing rendering/replay screenshots and E2E evidence remain applicable because executable code is unchanged.

已修复 review 指出的注释问题,说明 App 限制属于共享工具快照配置,因此必须参与连接池指纹。本次仅修改注释。连接池条目增加的权衡已补入 PR 中英文风险说明;超时信号与 SDK 使用同一截止时间的行为保持不变。全仓构建、类型检查、27 项连接池测试、Lint、格式检查和 diff 自查通过。

@wenshao

wenshao commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

Verdict: merge-ready — 828/828 scripted assertions passed, 0 unexpected failures. Verified head: e59d0f0a6e3ad2ba836561f5a5b54c8233c5fccb (single-commit PR; control build = its parent 42f9d13cda, so the A/B differs by exactly this PR).

中文摘要

结论:可以合并(merge-ready)——828/828 条脚本断言全部通过,无意外失败。

  • A/B 结论(见下方中央声明表格与证据图 01-ab-matrix-base-vs-head.png):默认 1 MiB / 10 秒行为与基线完全一致;配置 appResourceMaxBytes 后 1 MiB+1 和 2 MB 的 HTML 在 head 渲染、在 base 被拒绝;4 MiB 上限、100 ms 下限、120 s 上限三处钳制均经真实 stdio MCP 服务实测(含 120050 ms 实测钳制点);通用 timeout: 30000 不会抬高 App 的 10 秒默认截止(与 PR 描述一致)。
  • 连接池:两个新字段均进入池指纹(head 分区、base 不分区),不同资源策略不会复用同一池工具。
  • 取消语义:资源读取期间取消,两个 arm 均在 ~2 秒回退为纯文本显示,工具文本结果完整保留。
  • 测试非空洞性:将 boundedAppLimit 改为忽略配置值后,17 个目标测试中 13 个如期变红(含全部 6 个超时钳制用例、5/7 字节边界用例);恢复后 17/17 转绿(证据图 02-mutation-clamp-disabled.png)。4 个未变红的用例期望值恰等于回退值,属于正确的断言锚定而非空洞。
  • 门禁:head 347/347、base 328/328(+19 新测试,+0 失败);typecheck 活跃性探针(植入类型错误被捕获)后干净通过;main 自 PR 父提交以来未触碰本 PR 的任何文件,合并无冲突风险。
  • 未覆盖:WebShell 中 App 的实际渲染/交互/回放、真实 Amplitude 服务、settings.json 文件级端到端(已静态核实设置无 schema 剥离,且 harness 以与设置加载器相同的方式注入普通属性)。

Central claim and A/B proof

Claim: each MCP server can opt into a larger App HTML limit and a longer App resource-read deadline via appResourceMaxBytes / appResourceTimeoutMs; defaults stay 1 MiB / 10 s; explicit values are clamped to 4 MiB / [100 ms, 120 s]; the general MCP timeout alone must not raise the App deadline.

Method: a real stdio MCP fixture server (fixture-mcp-server.mjs, built on @modelcontextprotocol/sdk, scenario selected via argv) serves a chart tool whose _meta['ui/resourceUri'] points at ui://fixture/chart; resources/read returns HTML of an exact byte size after an exact delay. The driver (drive-cell.mjs) runs the worktree's own compiled packages/core/dist through connectToMcpServer → discoverTools → tool.build({}).execute(signal) — no mocks of the code under test — and records the returned McpAppResultDisplay, elapsed time, and the warning text. Every cell ran identically on both arms; the driver asserts the realpath of @qwen-code/qwen-code-core stays inside the arm's worktree before running.

Cell Fixture Config base (42f9d13cda) head (e59d0f0a)
S1 4,096 B HTML default renders 4096 B, 45 ms renders 4096 B, 41 ms
S2 1,048,577 B default rejected, "(1 MiB) host limit" rejected, "…host limit (mcpServers.fixture.appResourceMaxBytes)"
S3 1,048,577 B maxBytes 2 MiB rejected renders 1,048,577 B
S4 2,000,000 B maxBytes 8 MiB rejected renders 2,000,000 B (8 MiB clamped to 4 MiB still admits)
S5 4,194,305 B maxBytes 8 MiB rejected rejected, "exceeding the 4194304 byte host limit" — 4 MiB cap proven
T1 11 s delay default times out at 10055 ms, "limit: 10000 ms" times out at 10052 ms, message names appResourceTimeoutMs
T2 11 s delay timeout: 30000 only times out at 10041 ms times out at 10040 ms — general timeout does not raise the App cap
T3 11 s delay timeout: 30000 + App 15000 times out at 10054 ms renders after 11050 ms
T4 2 s delay App timeout 50 renders (field unknown) rejected at 140 ms, "limit: 100 ms" — min clamp proven
T5 130 s delay App timeout 200000 times out at 10043 ms rejected at 120050 ms, "limit: 120000 ms" — max clamp proven
C1 10 s delay, caller aborts at 2 s default text fallback at 2004 ms, tool text retained text fallback at 2003 ms, tool text retained

Every rejection preserved the successful tool result (llmContent still carried chart tool result ok); every head-side timeout warning named the responsible setting key. Matrix: 111/111 assertions passed (matrix-results.json). Witness: 01-ab-matrix-base-vs-head.png —

A/B matrix base vs head

Secondary claims

  • Pool separation. pool-key.mjs runs fingerprint() from each arm's compiled dist on configs differing only in the new fields: head → pool keys differ for both appResourceMaxBytes and appResourceTimeoutMs; base → identical (control proving the probe actually set the fields). Different resource policies cannot share a pooled tool.
  • Warnings are actionable. S2/T1 head cells carry mcpServers.<server>.appResourceMaxBytes / .appResourceTimeoutMs in the user-facing warning; the underlying cause is retained via debugLogger.warn ((cause: …)), so no information was suppressed.
  • Settings wiring (static census). mcpServers entries arrive as plain JSON objects cast to MCPServerConfig — no new MCPServerConfig(...) in the settings path and no stripping schema (the VS Code companion schema declares mcpServers as additionalProperties: true and never enumerates per-server keys, so there is no schema drift either). The harness sets the limits as plain properties, exactly how settings.json delivers them.

Reviewer Test Plan walk-through

  • "1,048,577 bytes: default warns and retains the tool result; 2 MiB renders" → S2/S3, both confirmed. The "remains interactive after transcript replay" half is not covered (WebShell rendering/replay is outside this harness; see below).
  • "11-second resource: general 30 s timeout still hits the App cap; explicit 15 s allows; shorter explicit rejects" → T2/T3/T4, all confirmed.
  • "Cancel during resource loading aborts the read and preserves the text result; different limits must not share a pooled policy" → C1 + pool-key probe, both confirmed.

Vacuity check (mutation A/B)

Mutation: boundedAppLimit in packages/core/src/tools/mcp-tool.ts replaced to ignore the configured value (interface-preserving; compiles clean). Targeted run of the PR's clamp/bounds tests:

  • Mutated: 13 of 17 failed — all 6 bounds the configured resource timeout cases, 5/7 enforces the configured HTML byte boundary cases, both sub-10 s rows of reports the resource timeout with MCP timeout. The 4 survivors are rows whose expectation equals the fallback value (NaN/Infinity → default, mcpTimeout ≥ 10 s → 10 s) — correct pinning, not vacuity.
  • Restored: 17/17 pass (control). Witness: 02-mutation-clamp-disabled.png —

Mutation vacuity run

Harness artifact worth noting for future rounds, not a product defect: two earlier mutation attempts appeared to "hang" — in fact vitest was rendering megabyte-scale assertion diffs on failed 1 MiB-string comparisons (loads larger configured … compares the full HTML on failure). Orphaned workers from those attempts were killed; the runs above used only small-diff fixtures.

Gates

Gate Result
Affected tests at head (mcp-tool, mcp-client, mcp-pool-key, toolResultDisplayCompaction) 347/347 pass
Same four files at base 328/328 pass (+19 new tests, +0 failures — no pre-existing failure attribution needed)
tsc --noEmit (packages/core, head) 0 errors — liveness probe first: a planted string = 42 produced TS6133+TS2322 as expected
Dist divergence check head dist contains appResourceMaxBytes (2 hits), base dist 0 — arms differ exactly by the PR
Merge drift main (8eeeed8218) has not touched any file this PR touches since the PR's parent — merge is trivially clean

Findings

None. No blocking or non-blocking findings from the exercised surface.

Not covered

  • WebShell rendering, interactivity, and cold-restart transcript replay of the App HTML (the author's own screenshots cover this on macOS; this round verified the core decision layer that admits/rejects the resource).
  • Real Amplitude service (out of scope per the PR's own Risk & Scope section).
  • Full CLI end-to-end from a settings.json file — the harness injects the limits as plain config properties exactly as the settings loader produces them (static census above); a model-in-the-loop run was not in budget.
  • Windows/macOS — this round ran on Linux aarch64 (Orange Pi 6 Plus, Node v24.14.0), complementing the author's macOS run; the changed code is platform-independent.
  • Per-commit verification: single-commit PR, aggregate diff verified.

Methodology

Local maintainer-driven round on Linux aarch64 (12-core, Node v24.14.0). Head and control were checked out as sibling worktrees (tmp/head-tree @ e59d0f0a, tmp/base-tree @ 42f9d13cda — the PR's parent, since baseRefOid is stale relative to the PR's single commit); package-lock.json is byte-identical across HEAD, parent, and head, so node_modules was hardlink-copied into both trees and only packages/core rebuilt in each (sibling workspace dists copied identically into both arms — the dependency layer is the same for both cells). The driver asserts realpath(node_modules/@qwen-code/qwen-code-core) resolves inside the arm's own worktree before every cell. Harnesses: fixture-mcp-server.mjs, drive-cell.mjs, run-matrix.mjs, pool-key.mjs (all in this artifact dir, rerunnable); raw per-cell server request logs under server-logs/; cell records in matrix-results.json. Total wall time ≈ 75 min including the 130 s max-clamp cell and the 120 s budget for diagnosing the megabyte-diff vitest wedge.


Maintainer-driven local verification round (/verify-pr protocol). Harnesses, raw logs, and cell records preserved locally under tmp/pr12258-verify-20260919-225033/.

@wenshao

wenshao commented Sep 19, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover from 2

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Sep 19, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This window's round counter starts at 2 (the rounds this PR spent in review before takeover), so the Critical-only brake engages after 3 more change-producing round(s) instead of a full fresh 5. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本窗口轮次计数从 2 起算(即本 PR 托管前已进行的评审轮数),因此再经过 3 个产生改动的轮次即进入 Critical-only,而非重新计满 5 轮。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

qwen-code-dev-bot commented Sep 19, 2026 •

Copy link
Copy Markdown
Collaborator

✅ AutoFix round 11 finished — view run. See this round's report below.

中文说明

✅ AutoFix 第 11 轮已完成 —— 查看运行。本轮报告见下方。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 AutoFix updated a stale base — the fix did not pass verification, but this PR was behind main, so it merged current main in via update-branch and will retry on the next scan. A stale base (a dependency or symbol main already changed) can fail the build without being the fix's fault; if it still fails once current, it hands off to a human.

What I found before stopping:
Qwen failed during address-review: status 143.

See the Qwen Autofix agent step logs for model/tool output.

中文说明

🤖 AutoFix 更新了一个过期的 base —— 修复未通过验证,但本 PR 落后于 main,因此已通过 update-branch 合入当前 main,并将在下次扫描时重试。过期的 base(main 已改动的依赖或符号)可能让构建失败而并非修复本身的错;若 base 更新后仍然失败,将移交人工处理。

Run log: https://github.com/QwenLM/qwen-code/actions/runs/35470296850


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.1

…2258)

Address review on the App resource limits change:

- WebShell historical-page admission now drops each MCP App display's
  html whole (never truncated) when a page exceeds its byte budget, so two
  ceiling-size documents degrade to fallbackText instead of failing the
  whole page as non-retryable unavailable; the design doc ceiling rationale
  now names all five budgets a retained document crosses.
- Pool fingerprint hashes App resource limits at their enforced
  (clamped/floored/defaulted) values via a shared module, so configs with
  byte-identical enforced policy share one pool entry.
- Limit warnings name the setting in the source that declares the server
  (extension manifest or .mcp.json) instead of always pointing at
  settings.json, whose same-named entry would shadow rather than merge.
- Timeout warnings credit the key that produced the deadline (timeout when
  no explicit App timeout is configured).
- A non-finite configured limit now logs a debug warning naming the server,
  key, rejected value, and fallback instead of being silently discarded.
- Add a reconnect-replay test pinning that App resource limits survive
  rediscovery, and sync the daemon transport-pool doc with the fingerprint
  contract.
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Addressed the latest review feedback (round 4/100). What changed, and what I pushed back on: · 已处理最新评审反馈(第 4/100 轮)。改动内容与我反驳保留之处如下:

Autofix round — PR #12258 review feedback

Commit: 269182b4e8 fix(mcp): Degrade oversized App HTML per historical page document (#12258)

Eight of the nine findings from the CHANGES_REQUESTED review are implemented and verified; the ninth (R1-2, carrying the two keys through the hand-enumerated config surfaces) is deferred to the next round with a reply on its thread — it is the widest change and this round's batch was already at its bound.

Dispositions

  • R1-1 (Critical, mcp-tool.ts:775 — 4 MiB ceiling vs five tighter budgets) — Fixed. Historical-page admission in packages/web-shell/client/daemon/session/transcript-page-table.ts now degrades per document instead of failing per page: fitPageWithinBudget blanks every MCP App display's html whole (the documented degrade path — replay mounts the iframe only for non-empty html and never re-fetches the ui:// resource) and re-measures, throwing HistoricalTranscriptPageTooLargeError only when nothing degradable remains. Applied at all three admission sites (sequential, anchored, boundary), with the empty-page branch kept ahead of the fit so its window-full semantics are unchanged. Companion test in turn-navigation-store.test.ts uses the default 16 MiB budget with two 4 MiB mcp_app displays and asserts locateOrdinal resolves with selected.status: 'ready' and the retained blocks carry html: '' with intact fallbackText. The design doc's ceiling rationale (EN + zh-CN) now names all five budgets a retained document crosses, in their own units (16 MiB historical page estimated as UTF-16 code units ×2; 4 MiB compacted replay window; 2 MiB EventBus per-subscriber live frame; 32 MiB JSON restore page; 32 MiB replay envelope) and states the per-document degradation rule.
  • R1-2 (Suggestion, mcp-server-config.ts:161 — keys dropped by hand-enumerated config surfaces) — Deferred to the next round (batch bound; widest of the nine). A reply with the planned shape is posted on its thread.
  • R1-3 (Suggestion, mcp-tool.ts:821 — warning names a settings path that shadows extension/project servers) — Fixed. appLimitSettingRef prints the mcpServers.<name>.<key> path only for settings-declared servers and otherwise names the declaring extension or .mcp.json; provenance (extensionName, scope) rides the limits object built at discovery, so it survives all tool projections including the reconnect replay leg. Two new tests assert the warning names the declaring source and never the settings path; docs/users/features/mcp.md now says the keys must be set in the source that declares the server because same-named entries replace whole server objects.
  • R1-4 (Suggestion, mcp-pool-key.ts:145 — fingerprint hashes raw values while enforcement clamps) — Fixed. New shared module packages/core/src/tools/mcp-app-resource-limits.ts owns the four bounds plus the clamp; the read site and the fingerprint both consume it, so they cannot drift. The key now hashes enforced values: unset ≡ explicit 1 MiB default, over-ceiling values collapse, and a quoted "4194304" hashes like unset (matching its read-site behavior). The timeout arm hashes null for unset/invalid because the effective fallback derives from timeout, which is hashed separately. The over-ceiling appResourceTimeoutMs arm of the isolation test was inverted to in-range values (20 s vs 30 s), and a new test pins the normalization equivalences.
  • R1-5 (Suggestion, docs/users/features/mcp.md:497 — guidance omits where App HTML is mounted) — Fixed with one paragraph: App HTML is mounted only in daemon-backed WebShell sessions and in replay of a recorded transcript; terminal and headless sessions render the fallback text, so there the keys change only the warning text, fetch cost/latency, and retained transcript size.
  • R1-6 (Suggestion, mcp-tool.ts:846 — timeout warning always names appResourceTimeoutMs) — Fixed. The warning credits the key that produced the deadline, computed with the same finite-number test the clamp uses so a discarded non-numeric override is not credited: appResourceTimeoutMs only when an explicit finite value is set, otherwise timeout (covering both a configured general timeout and the 10 s default). The four mcpTimeout-only table rows now expect mcpServers.<server>.timeout; a new row with an explicit App timeout keeps expecting appResourceTimeoutMs.
  • R1-7 (Suggestion, mcp-tool.ts:294 — non-finite configured limit silently discarded) — Fixed. appResourceLimit emits one debugLogger.warn naming the server, the key, the rejected value, and the fallback now in effect; behavior (log, not throw) is unchanged, and the NaN/Infinity fallback-value cases still pass. Both boundary tables gained a quoted-string case plus warn assertions (valid rows assert no discard warn).
  • R1-8 (Suggestion, mcp-tool.ts:520 — reconnect/replay propagation leg untested) — Fixed. New test under auto-reconnect on connection error: the first client's callTool rejects with Connection closed, ensureTool resolves a re-discovered tool carrying appResourceUri and a 2 MiB appResourceMaxBytes, and a 1 MiB+1 HTML resource renders through the replay with no warning in fallbackText.
  • R1-9 (Suggestion, mcp-pool-key.ts:116 — daemon pool doc contradicts the fingerprint contract) — Fixed. docs/developers/daemon/05-mcp-transport-pool.md now lists the two fields as hashed, retitles the category to transport + shared tool-snapshot settings, and states the rule next to the discoveryTimeoutMs paragraph: a field consumed at discovery and never re-projected per session belongs in the key even when it is not transport-defining. Scoped to the two fields this PR adds; the pre-existing authProviderType/targetAudience/targetServiceAccount list drift is left alone as the finding instructed.

Mutation probes

  • R1-1: forced fitPageWithinBudget to fail closed → new degradation test reds; restored → green.
  • R1-3: removed the provenance branches → both declaring-source cases red; restored → green.
  • R1-4: reverted the fingerprint to raw values → hashes App resource limits at their enforced values reds; restored → green.
  • R1-6: made attribution unconditional → exactly the four .timeout rows red, the appResourceTimeoutMs row stays green; restored → green.
  • R1-7: silenced the discard warn → all six warned rows (NaN/Infinity/string × bytes/timeout) red; restored → green.
  • R1-8: dropped newTool.appResourceLimits from the replay constructor → reconnect replay test reds; restored → green.

Verification

  • npm run build — the monolithic command exceeds this runner's 120 s foreground ceiling (and this harness dropped my attempts to raise the timeout or detach it), so I ran every constituent step to green instead: npm run generate, each workspace build in dependency order (browser-use, core, all channels, audio-capture, node-repl, acp-bridge, sdk-typescript, web-shell, web-templates, cli, qwen-live, vscode-ide-companion, chrome-extension, both integrations/), plus node --import tsx/esm scripts/generate-settings-schema.ts (regenerated; no git drift). A partial dist left by a killed run was diagnosed (@qwen-code/web-shell/transcript missing) and repaired by rebuilding web-shell before web-templates/cli.
  • npm run typecheck --workspace=packages/core --workspace=packages/web-shell --workspace=packages/sdk-typescript — passed, 0 errors.
  • npx tsc -p integration-tests/tsconfig.json --pretty false — passed, 0 errors.
  • npx eslint on all 12 changed files — passed, 0 problems. npx prettier --check — one new file reformatted, then clean.
  • npx vitest run src/tools (packages/core) — 3792 passed, 1 failed: todoWrite.test.ts expects the default ~/.qwen/todos path, but this runner exports QWEN_HOME=/…/qwen-autofix-review-home; with env -u QWEN_HOME the file passes 57/57. Environmental, unrelated to this diff (those files are untouched).
  • npx vitest run client/daemon/session (packages/web-shell) — 992 passed.
  • Focused re-run on the committed tree — mcp-tool + mcp-pool-key 171 passed; turn-navigation-store + transcript-page-table 105 passed.
中文说明

Autofix 本轮处理 — PR #12258 评审意见

提交:269182b4e8 fix(mcp): Degrade oversized App HTML per historical page document (#12258)

CHANGES_REQUESTED 评审中的九条意见,本轮实现并验证了八条;第九条(R1-2,把两个配置键打通到各手工枚举的配置面)推迟到下一轮,并已在该意见的讨论串中回复说明——它是九条中涉及面最广的一条,而本轮批次已达到上限。

处理结论

  • R1-1(Critical,mcp-tool.ts:775——4 MiB 上限与五个更紧的预算)——已修复。packages/web-shell/client/daemon/session/transcript-page-table.ts 中的历史页准入改为按文档降级而非整页失败:fitPageWithinBudget 在页超过保留字节预算时,将每个 MCP App 展示的 html 整体置空(文档写明的降级路径——回放只在 html 非空时挂载 iframe,且不会重新拉取 ui:// 资源)并重新计量;只有在无可降级内容时才抛出 HistoricalTranscriptPageTooLargeError。三个准入点(顺序页、锚定页、边界页)都已接入,空页分支保持在适配检查之前,其"窗口已满"语义不变。配套测试位于 turn-navigation-store.test.ts:使用默认 16 MiB 预算和两个各 4 MiB 的 mcp_app 展示,断言 locateOrdinal 正常解析、selected.status 为 'ready',且保留块中 html 为 ''、fallbackText 完整。设计文档的上限论证(中英文)现在按各自单位点名了保留文档会穿过的全部五个预算(16 MiB 历史页,按 UTF-16 码元乘二估算;4 MiB 压缩回放窗口;2 MiB EventBus 每订阅者实时帧;32 MiB JSON 恢复页;32 MiB 回放信封),并写明按文档降级规则。
  • R1-2(Suggestion,mcp-server-config.ts:161——配置键被手工枚举的配置面丢弃)——推迟到下一轮(批次上限;九条中最广)。已在该讨论串回复计划的实现形态。
  • R1-3(Suggestion,mcp-tool.ts:821——告警点名的 settings 路径会遮蔽扩展/项目服务)——已修复。appLimitSettingRef 只对 settings 声明的服务打印 mcpServers.<name>.<key> 路径,否则点名声明它的扩展或 .mcp.json;来源信息(extensionName、scope)随发现阶段构造的 limits 对象传递,因此在包括重连重放在内的所有工具投影中都能保留。两个新用例断言告警点名声明来源且不再出现 settings 路径;docs/users/features/mcp.md 已说明这些键必须设置在声明该服务的来源中,因为同名条目会整体替换服务对象。
  • R1-4(Suggestion,mcp-pool-key.ts:145——指纹哈希原始值而执行处钳制)——已修复。新增共享模块 packages/core/src/tools/mcp-app-resource-limits.ts 统一持有四个边界值与钳制函数,读取点与指纹共同消费,不会漂移。键现在哈希生效值:未设置 ≡ 显式 1 MiB 默认值,超上限值收敛,字符串 "4194304" 与未设置哈希相同(与其读取点行为一致)。超时键在未设置/无效时哈希为 null,因为生效回退值由已单独入键的 timeout 推导。隔离测试中超上限的 appResourceTimeoutMs 分支已改为区间内取值(20 s 对 30 s),并新增一个测试钉住这些归一化等价关系。
  • R1-5(Suggestion,docs/users/features/mcp.md:497——指引未说明 App HTML 的挂载位置)——已修复,新增一段:App HTML 只在守护进程支撑的 WebShell 会话和录制会话记录的回放中挂载;终端与无头会话渲染降级文本,因此在那里这两个键只改变告警文案、拉取开销/延迟和保留的会话记录体积。
  • R1-6(Suggestion,mcp-tool.ts:846——超时告警无条件点名 appResourceTimeoutMs)——已修复。告警改为点名实际产生截止时间的键,判定使用与钳制相同的有限数值测试,被丢弃的非数值覆盖项不会被误记:仅在设置了显式有限值时点名 appResourceTimeoutMs,否则点名 timeout(涵盖已配置通用超时与 10 秒默认两种情况)。四个仅含 mcpTimeout 的表格用例现在期望 mcpServers.<server>.timeout;新增的显式 App 超时用例继续期望 appResourceTimeoutMs。
  • R1-7(Suggestion,mcp-tool.ts:294——非有限配置值被静默丢弃)——已修复。appResourceLimit 输出一条 debugLogger.warn,点名服务、键、被拒值和当前生效的回退值;行为不变(记录日志而非抛错),NaN/Infinity 回退值用例继续通过。两个边界表格都新增了带引号字符串用例和告警断言(有效值用例断言没有丢弃告警)。
  • R1-8(Suggestion,mcp-tool.ts:520——重连/重放传播点无测试覆盖)——已修复。auto-reconnect on connection error 下新增用例:第一个 client 的 callTool 以 Connection closed 拒绝,ensureTool 解析出携带 appResourceUri 与 2 MiB appResourceMaxBytes 的重发现工具,一份 1 MiB+1 的 HTML 资源经重放正常渲染且 fallbackText 不含告警。
  • R1-9(Suggestion,mcp-pool-key.ts:116——守护进程连接池文档与指纹契约矛盾)——已修复。docs/developers/daemon/05-mcp-transport-pool.md 现在把这两个字段列入被哈希清单,类别标题改为"传输 + 共享工具快照设置",并在 discoveryTimeoutMs 段落旁写明规则:凡在发现阶段被消费且从不按会话重新投影的字段都必须入键,即便它不定义传输。改动范围按意见要求限定在本 PR 新增的两个字段;清单此前缺失 authProviderType/targetAudience/targetServiceAccount 的既有漂移未一并处理。

变异探针

  • R1-1:强制 fitPageWithinBudget 失败关闭 → 新降级测试变红;恢复 → 转绿。
  • R1-3:移除来源分支 → 两个声明来源用例变红;恢复 → 转绿。
  • R1-4:指纹回退为原始值 → hashes App resource limits at their enforced values 变红;恢复 → 转绿。
  • R1-6:归因改为无条件 → 恰好四个 .timeout 行变红,appResourceTimeoutMs 行保持绿色;恢复 → 转绿。
  • R1-7:静默丢弃告警 → 全部六个 warned 行(NaN/Infinity/字符串 × 字节/超时)变红;恢复 → 转绿。
  • R1-8:从重放构造器中去掉 newTool.appResourceLimits → 重连重放测试变红;恢复 → 转绿。

验证

  • npm run build——整体命令超过本运行器 120 秒前台上限(且本环境丢弃了我提高超时或改为后台的尝试参数),因此改为把每个组成步骤分别跑到绿:npm run generate,按依赖顺序逐个工作区构建(browser-use、core、全部 channels、audio-capture、node-repl、acp-bridge、sdk-typescript、web-shell、web-templates、cli、qwen-live、vscode-ide-companion、chrome-extension、两个 integrations/),以及 node --import tsx/esm scripts/generate-settings-schema.ts(已重新生成,无 git 漂移)。一次被杀死的运行留下的残缺 dist(缺少 @qwen-code/web-shell/transcript)已被诊断并通过在 web-templates/cli 之前重建 web-shell 修复。
  • npm run typecheck --workspace=packages/core --workspace=packages/web-shell --workspace=packages/sdk-typescript——通过,0 错误。
  • npx tsc -p integration-tests/tsconfig.json --pretty false——通过,0 错误。
  • 对全部 12 个改动文件执行 npx eslint——通过,0 问题。npx prettier --check——一个新文件被重排格式,随后全部通过。
  • npx vitest run src/tools(packages/core)——3792 通过,1 失败:todoWrite.test.ts 期望默认的 ~/.qwen/todos 路径,但本运行器导出了 QWEN_HOME=/…/qwen-autofix-review-home;用 env -u QWEN_HOME 运行时该文件 57/57 通过。属于环境问题,与本 diff 无关(这些文件未被触碰)。
  • npx vitest run client/daemon/session(packages/web-shell)——992 通过。
  • 在已提交树上聚焦复跑——mcp-tool + mcp-pool-key 171 通过;turn-navigation-store + transcript-page-table 105 通过。

🧭 Gate advisory — this round modified areas outside the PR footprint (machine-measured, not agent-authored):

  • packages/web-shell
    Review the expansion deliberately; the footprint gate is in advisory mode. · 本轮改动了 PR 足迹之外的区域(门自动测量,非 agent 文本),当前足迹门为 advisory 模式,请有意识地审阅该扩张。

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

🧵 Resolved all 8 selected review thread(s). · 已关闭全部选中的 8 条评审线程。

Re-review when you have a moment. After round 100 this bot stops and leaves the PR for a human. · 有空请复审;第 100 轮后本 bot 停止并将 PR 交给人工。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.1

@samuelhsin

Copy link
Copy Markdown
Collaborator Author

Local follow-up completed at 4bf5e1864f (two focused fixes):

  • R1-1, remaining live-delivery failure: reproduced a 3 MiB App disconnecting the client with one pending progress event, and the symmetric App-first/ordinary-event-next case. Queued consumers now receive the completed fallback text; waiting consumers and the reconnect ring keep the original HTML. App fallbacks that still exceed the byte budget remain rejected. Frame-count and forced-replay rules are unchanged.
  • R1-2, configuration loss: both settings now survive TypeScript SDK schema/types, CLI initialization, and daemon settings read/write for stdio, HTTP and SSE. Zero, negative and fractional finite numbers are preserved for the core's existing floor/clamp policy. Existing route ownership and settings scope are unchanged.

Validation: full repository build, bundle and typecheck; 64 bridge + 354 core + 105 WebShell + 796 CLI + 105 SDK tests = 1,424 passed; SDK public-type compiler fence; ESLint and Prettier. New disconnect and configuration tests failed before the fixes and passed afterward. Independent verification exercised seven live-queue scenarios, five historical-page/navigation scenarios, SDK initialization and actual node dist/cli.js --acp settings writes, disk persistence and reads for all three transports. Two clean self-audit passes and independent correctness review found no outstanding blocker in these changes.

Real local WebShell historical App navigation

The screenshot uses synthetic data and shows a 4 MiB App with subsequent conversation text. The existing pinned historical-window limit can still require navigating to an adjacent turn and back; the real browser verified recovery, so that separate memory-protection behavior was preserved. Authenticated Amplitude rendering remains unverified because OAuth was not completed.

The non-blocking timeout-warning attribution issue is deferred: when the default 10-second cap supplies the deadline, the current warning names the general timeout although raising it cannot override that cap; an explicit App timeout is required. This does not change execution or acceptance behavior. Following the repository's review-scope rule after roughly five rounds, this wording refinement is left out of the two correctness fixes.

本轮已修复剩余实时断连及配置丢字段问题,推送两个独立提交。全量构建、类型检查、1,424 项相关测试和独立本地验证通过;截图为真实 WebShell 上的合成数据。原有历史缓存窗口限制可通过邻近对话导航恢复,保持不变。真实 Amplitude 仍待 OAuth 验证。默认超时警告的归因措辞作为非阻塞改进留待后续,避免本轮继续扩大修复范围。

@samuelhsin

Copy link
Copy Markdown
Collaborator Author

Authenticated Amplitude follow-up on 224cd3567e:

  • Actual OAuth + MCP resources/read: 2,333,981 UTF-8 bytes, identical on two reads; 88.715 s / 79.107 s complete-read durations. 4 MiB is enough for this resource version, while the default 1 MiB / 10 s is insufficient. The Amplitude example now uses 4 MiB / 120 s; the previous 30 s example would fail both measured reads.
  • Actual core and bundled CLI accepted the unmodified captured HTML with the override and rejected it with defaults. The accepted HTML's SHA-256 matches the original, including the persisted transcript. Local replay was not used to claim remote timing behavior.
  • The real resource executes in WebShell, but displays “Host does not support fetching chart data.” The tool result for this browser test contains a synthetic chart reference, not real analytics; it does not inject chart data to bypass the App's data-fetch path. Full chart compatibility is therefore not fixed or claimed by this PR. App-initiated tool/data requests remain a follow-up; MCP Apps: Amplitude chart UI rejected by fixed 1 MiB HTML limit and 10s resource timeout #11945 explicitly scopes this PR to resource-loading limits.

Real Amplitude resource in local WebShell

Only the screenshot is published. OAuth tokens stayed in memory; raw HTML and tool metadata stayed in a private local temporary directory. Temporary test servers were stopped. Documentation-only change: inspected diff and git diff --check; no production code changed after the prior full build/typecheck and 1,424 targeted tests.

已完成真实 OAuth 实测:2.23 MiB,79–89 秒。4 MiB 足以容纳本次资源,文档超时示例修正为 120 秒。真实 HTML 原样通过配置后的 core/CLI 并在 WebShell 运行,但图表仍因宿主缺少数据获取能力而失败;截图明确记录该剩余限制,未声称 Amplitude 完整兼容已修复。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

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

Merge resolution — PR #12258 ← origin/main

One conflict, one file: packages/core/src/tools/tool-registry.ts; merge commit 0dcbf8b12a. The merged tree is exactly the union of both sides — git diff HEAD^2 HEAD touches only PR files, git diff HEAD^1 HEAD only main files — and no markers remain.

Root cause

Main picked up #12539 (99fd76553e), which extended a comment inside the inline tool-removal loop of discoverToolsForServer. This PR deleted that loop, delegating to the pre-existing removeMcpToolsByServer() and adding a reconnect flag. Same lines, two edits → conflict. The rest of #12539 auto-merged.

Textual, not semantic

Main changed prose; the PR changed the code that prose described. Delegation is behaviour-preserving because removeMcpToolsByServer is a strict superset of main's loop — same this.tools sweep, same revealedDeferred.delete(name), plus a purge of the PR's new mcpAppTools:

async discoverToolsForServer(serverName: string, reconnect = false) {
  this.removeMcpToolsByServer(serverName);
  // prompts + resources removal unchanged
  await this.mcpClientManager.discoverMcpToolsForServer(
    serverName, this.config, reconnect);
}

That sentence being main's whole edit, I carried it to where the deletion now lives — appended to the existing comment in removeMcpToolsByServer: "…The reviewed-declaration record is deliberately left alone: see reviewedDeferredDeclarations." Nothing else outside the hunk changed.

Load-bearing

  • removeMcpToolsByServer / removeDiscoveredTools must keep not touching reviewedDeferredDeclarations: a pruned entry reads as "never reviewed" and waves a replacement tool through — the bug Deferred review findings from PR #10410: feat(core): preserve prompt cache for deferred tools #11321 closed.
  • reconnect must stay defaulted false; every pre-existing caller passes one argument.
  • The mcpAppTools purge must run before discoverMcpToolsForServer, or a reconnect leaves the previous connection's app-only tools callable.

Not verified

No build, typecheck, lint or tests run here. tool-registry.test.ts auto-merged with new tests from both sides: main's "keeps the reviewed declaration after removal" (~L1147) and "removeMcpToolsByServer also drops revealedDeferred entries" (~L1122) now run against the PR's helper, which also clears mcpAppTools. Both should still hold — those tools are built without appVisibility, so both visibility getters are true — but I did not execute them.

中文说明

唯一冲突文件 packages/core/src/tools/tool-registry.ts,合并提交 0dcbf8b12a;合并结果恰为两侧改动的并集。

根因:main 合入 #12539(99fd76553e),在 discoverToolsForServer 的内联移除循环里补了一句注释;本 PR 则把整段循环换成调用既有的 removeMcpToolsByServer() 并新增 reconnect 参数。同一处、两种改法 → 冲突;#12539 其余内容自动合并。

属于文本冲突:委托调用等价——removeMcpToolsByServer 是那段循环的严格超集(同样清 this.tools 与 revealedDeferred,外加清 PR 新增的 mcpAppTools)。main 那句注释被移到真正执行删除的 removeMcpToolsByServer;此外未改动冲突块以外的代码。

关键约束:两个移除方法必须继续不清理 reviewedDeferredDeclarations(否则条目被读成"从未审阅",会放行被替换的工具,即 #11321 所修缺陷);reconnect 必须保持默认 false(现有调用方都只传一个参数);mcpAppTools 的清理必须在 discoverMcpToolsForServer 之前(否则重连后旧连接的 app-only 工具仍可调用)。

未验证:未跑构建、类型检查、lint 或测试。tool-registry.test.ts 自动合并后含两侧新测试,main 的两个用例(约 L1147、L1122)现在跑在本 PR 的 helper 上,按阅读仍应通过(用例未传 appVisibility),但未实际运行。

@wenshao

wenshao commented Sep 26, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /takeover from 10

@qwen-code-dev-bot qwen-code-dev-bot added the autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+) label Sep 26, 2026
@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤝 Takeover engaged: the autofix loop now manages this PR — it will address new review feedback and resolve base conflicts until the label is removed or the round cap is reached. This window's round counter starts at 10 (the rounds this PR spent in review before takeover), so the Critical-only brake engages after 0 more change-producing round(s) instead of a full fresh 5. Remove the autofix/takeover label (or comment @qwen-code /takeover stop) to release.

中文说明

🤝 已接管:autofix 循环现在管理此 PR —— 将持续处理新的评审反馈与 base 冲突,直到移除标签或达到轮次上限。本窗口轮次计数从 10 起算(即本 PR 托管前已进行的评审轮数),因此再经过 0 个产生改动的轮次即进入 Critical-only,而非重新计满 5 轮。移除 autofix/takeover 标签(或评论 @qwen-code /takeover stop)即可释放。

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🔀 Base updated: red check(s) [TUI parity snapshots (ink vs opentui), Test (ubuntu-latest, Node 22.x), OpenTUI no-flicker gate, Lint & Static (ubuntu-latest, Node 22.x), Integration Tests (no-AK, No Sandbox), web-shell E2E Smoke (ubuntu-latest, Node 22.x)] pass on current main — merged current main via update-branch; CI will re-run.

中文说明

🔀 已更新 base:红色检查 [TUI parity snapshots (ink vs opentui), Test (ubuntu-latest, Node 22.x), OpenTUI no-flicker gate, Lint & Static (ubuntu-latest, Node 22.x), Integration Tests (no-AK, No Sandbox), web-shell E2E Smoke (ubuntu-latest, Node 22.x)] 在当前 main 上通过 —— 已通过 update-branch 合入当前 main,CI 将重新运行。

yunlong1020 and others added 2 commits September 26, 2026 15:58
The main merge left sourceSessionId twice on TranscriptViewport, which fails the web-shell typecheck and stops every job at install.

Co-authored-by: Cursor <[email protected]>
@samuelhsin

Copy link
Copy Markdown
Collaborator Author

R11 follow-up on cab293b: 0d6eb9e fixes the two standing R8 findings, with no new feature scope.

  • R8-1: resource timeout warnings now point to the App-specific setting when raising the general timeout cannot exceed the enforced ceiling. Enforced deadlines are unchanged.
  • R8-3: failed initialization handoffs revoke the App tool callback, abort calls and close/unload the App. Late failures from an old mount cannot hide or clear its replacement.
  • R8-2 was already fixed by cab293b; exactly one sourceSessionId binding remains, and the full build succeeds.

Validation: frozen pnpm installation, full build and bundle, full workspace/integration typechecks, focused lint/format checks, 172 core tests and 38 App component tests passed. Three independent DOM probes passed, including a genuinely pending request cancelled at the component/host-adapter boundary. Minimal temporary mutations proved the new checks fail when the timeout attribution branch or input/result teardown is removed, and when the timeout comparison is inverted. These are core/component checks, not a new vendor or remote browser acceptance run.

R1-1 was rechecked against the exact tree. Replacing a very short HTML payload with a fallback can increase bytes, but the page accounts for the signed byte difference and rejects an over-budget result. Both replay-envelope callers revalidate the serialized envelope after their bounded degradation pass. I found no confirmed budget bypass or unbounded retry; this does not claim every oversized page can be recovered. The truncated unresolved review note provides no additional trigger to reproduce.

The maintainer-reproduced cross-tab HTTP/1.1 wedge remains open, as previously recorded; this commit does not solve it. The event-stream delivery proposal still needs the stated maintainer decision about pre-merge work versus a follow-up. The existing non-critical suggestions remain deferred under the convergence rule, and earlier Amplitude/Tableau evidence retains its original scope. The core change in this round is limited to diagnostic attribution; no timeout or tool-execution policy changed.

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix review round — no change (verification-only)

Head verified: 45c260aad1383a05bc83cd2c3456a781fef3ae56 (merge of main; includes 0d6eb9e2db and cab293b3dd). Working tree was clean before and after this round; no commit was created.

Critical-only mode is active for this window (seeded at round 10 by takeover). This round re-verified every actionable Critical finding against the exact tree and found all of them fixed at HEAD. The failed-checks list is empty. The one remaining functional item is an explicit maintainer decision, which this round deliberately does not settle.

Standing Critical findings — verified fixed at HEAD

Finding Evidence at HEAD
R8-1 (resource-timeout warning names the wrong setting) mcp-tool.ts:942-946 resolves timeoutKey to appResourceTimeoutMs when an explicit finite App timeout is configured or the inherited timeout is capped at MCP_APP_RESOURCE_TIMEOUT_DEFAULT_MS, and to timeout otherwise. The pinning it.each table (mcp-tool.test.ts:2232-2329) flips expectedKey exactly at that boundary.
R8-3 (post-oninitialized handoff failure leaves a live bridge) All three handoffs (sendSandboxResourceReady, sendToolInput/sendToolResult, connect) route to failInitialization (McpApp.tsx:260-271, wired at the three handoffs :290/:302/:323), which is generation-guarded, revokes oncalltool, aborts in-flight calls, removes the iframe src, clears bridgeRef, closes the bridge, and sets the visible error. The stale-remount case is covered by a dedicated test.
R8-2 (duplicate sourceSessionId JSX attribute) ChatPane.tsx:1722 carries exactly one sourceSessionId={connection.sessionId} on <TranscriptViewport>; the full build passes.
R6-1 (telemetry catalog drift) The route audit asserts 75 unique routes with a 73 handler-resolved / 2 pre-resolved split (telemetry.test.ts:1147-1158).
R5-1 … R5-5 Foundation docs (both languages) describe the real data-mode response; failInitialization clears bridgeRef before closing; the App catch path repairs the connection via shouldAttemptReconnect and then throws the sanitized error without replay (mcp-tool.ts:777-792); the SDK App-call timeout is 310_000 ms (DaemonClient.ts:6531); App permission admission is capped at min(8, max(0, maxPendingPerSession - 1)) per session (bridgeClient.ts:1013-1018).
R4-1 … R4-6 Remote/unreachable isolated origins select the data-document path via resolveMcpAppSandboxUrl plus a bounded 10 s retry; a 30 s progress heartbeat uses the declared progressToken; live-queue degradation is gated on a nonempty queue (eventBus.ts:1224-1226); both reference docs describe the lossy subscriber-specific payload; beginClose() no longer aborts App calls (only dispose() does — Session.ts:4492); the replay window degrades oldest-first before evicting and touches the newest segment only when eviction cannot make it fit (compactionEngine.ts:909-936).
R3-1 / R3-2 Foundation docs (EN + zh-CN) state both frames grant allow-same-origin with the immutable HTTP-CSP enforcement; dropMcpAppHtml substitutes a visible omission notice when fallbackText is empty (transcript-page-table.ts:126-149).
B1 / F1 / F2 (maintainer rounds 2–4) Content-Security-Policy: sandbox … is delivered as an HTTP header on both sandbox responses (mcp-app-sandbox.ts:197,256); a page-wide App-call slot limit of 2 with a shared queue keeps approval requests unblocked (McpApp.tsx:124-159); the invocation guard is skipped only for App repair-without-replay (mcp-tool.ts:586). The maintainer independently re-verified all three in his rounds 3, 5 and 6.

R1-1 (aggregate App-HTML budget) — traced at HEAD, no confirmed bypass

The standing "unresolved, please confirm" item was re-traced leg by leg against the exact tree:

  • Page assembly (history-replay-page.ts, replayContext.sendUpdate): the overflow pass blanks App html oldest-first using signed byte deltas (serializedUpdateBytes -= old − new), so a replacement that grows the payload (a very short html swapped for the longer omission notice) increases the accounted size instead of hiding it. The loop is bounded by the page length, and a page still over budget after the delivered update itself is degraded throws HistoryReplayLimitError — an explicit failure, not a silently over-budget page.
  • Envelope backstop (degradeReplayEnvelopeAppHtml): same signed accounting and bounded loop, and both callers (acpAgent.ts:5924, acpAgent.ts:6100) re-run validateLoadReplayEnvelope immediately afterwards.
  • Replay window (compactionEngine.ts): two-phase degrade with signed deltas, eviction as the backstop, newest segment degraded only when eviction cannot make the window fit.
  • Live queue (eventBus.ts): degradation only when a nonempty backlog would overflow, and frames with empty fallbackText are never degraded into empty content.
  • Historical page (transcript-page-table.ts): html is dropped whole and replaced with the omission notice when the fallback is empty.

No silent over-budget admission, no unbounded loop, and no empty-content frame was found in any leg; a document that cannot be degraded enough fails with an explicit, visible error. This matches the author's R11 recheck. The two related fails-closed hardening notes from rounds 6/8 (newest-segment eviction order; non-shrinking loop exit) sit in this window's deferred audit record and were not touched.

Maintainer items

  • Cross-tab connection wedge (maintainer rounds 5–6) — maintainer decision, left open. The per-page cap fixed the single-tab case (maintainer-verified), but the browser's 6-connection-per-host budget is shared across tabs, so 3 tabs × 1 pending App call can still starve the approval request. The maintainer explicitly framed this as his call: merge as-is and track the event-stream delivery (round-4 option (a)) as a follow-up, or require it before merge. This round does not pick a contested structural direction on his behalf; the question stands for @wenshao.
  • Cheap follow-ups named by the maintainer — not done this round, reason recorded. F-3 (String.replace $-pattern corruption at the two __HOST_ORIGIN__ substitution sites, mcp-app-sandbox.ts:199,262 — fix is a replacer function), F-2 (refuse new mints at the 256-registration cap instead of evicting), and the live-vs-replay App-call count mismatch. The maintainer classified all three as follow-ups that do not block merge, and this window is Critical-only; growing a merge-ready diff for non-Critical items works against the convergence policy. They remain recorded for the follow-up pass.
  • Remote HTTPS full-deployment acceptance remains the author-tracked limitation recorded in the PR description; no new evidence or request arrived in this window.

Deferred non-Critical feedback

The bot Suggestions listed in this window's "Deferred non-Critical feedback" section are an audit record; per the Critical-only brake they were not worked, resolved, or replied to.

Verification

  • npm run build — passed (run with COREPACK_HOME=/tmp/corepack-home; the default $HOME corepack cache is not writable in this sandbox).
  • vitest run src/tools/mcp-tool.test.ts (packages/core) — 172 passed.
  • vitest run client/components/messages/McpApp.dom.test.tsx client/components/messages/McpApp.test.ts (packages/web-shell) — 38 passed.
  • vitest run src/serve/mcp-app-sandbox.test.ts src/serve/server/telemetry.test.ts src/serve/server/telemetry-catalog.test.ts (packages/cli) — 88 passed.
  • vitest run src/acp-integration/session/history-replay-page.test.ts (packages/cli) — 33 passed.
  • vitest run src/eventBus.test.ts src/compactionEngine.test.ts (packages/acp-bridge) — 200 passed.
  • vitest run src/bridgeClient.test.ts (packages/acp-bridge) — 149 passed.
  • vitest run test/unit/DaemonClient.test.ts (packages/sdk-typescript) — 489 passed.
  • npm run typecheck — passed (full workspace, unchanged tree).

Total: 1,169 focused tests passed at the unchanged head. No source or test file was modified; git status is clean.

中文说明

Autofix 审查轮次 —— 无改动(仅验证)

验证的 head: 45c260aad1383a05bc83cd2c3456a781fef3ae56(合并 main;包含 0d6eb9e2db 与 cab293b3dd)。本轮前后工作树均干净,未创建提交。

本窗口处于仅 Critical 模式(由 takeover 从第 10 轮起算)。本轮对当前代码逐项复核了所有可执行的 Critical 发现,确认它们在 HEAD 均已修复。失败检查列表为空。唯一剩余的功能性事项是维护者明确需要自己决定的问题,本轮有意不代为定夺。

未决 Critical 发现 —— 已在 HEAD 验证修复

发现 HEAD 上的证据
R8-1(资源超时警告指向错误设置) mcp-tool.ts:942-946 在配置了显式有限 App 超时、或继承超时被封顶到 MCP_APP_RESOURCE_TIMEOUT_DEFAULT_MS 时将 timeoutKey 解析为 appResourceTimeoutMs,否则为 timeout。钉住该行为的 it.each 表(mcp-tool.test.ts:2232-2329)恰好在此边界翻转 expectedKey。
R8-3(oninitialized 之后交接失败仍保留活桥) 三个交接点(sendSandboxResourceReady、sendToolInput/sendToolResult、connect)都进入 failInitialization(McpApp.tsx:260-271,三个交接点 :290/:302/:323 接入):带 generation 守卫,撤销 oncalltool,中止在途调用,移除 iframe 的 src,清空 bridgeRef,关闭桥接并设置可见错误。过期重挂载场景有专门测试覆盖。
R8-2(重复的 sourceSessionId JSX 属性) ChatPane.tsx:1722 的 <TranscriptViewport> 上只保留一个 sourceSessionId={connection.sessionId};完整构建通过。
R6-1(遥测路由目录漂移) 路由审计断言 75 条唯一路由、73 条 handler 解析 / 2 条预解析(telemetry.test.ts:1147-1158)。
R5-1 … R5-5 基础设计文档(中英双语)已按真实 data 模式响应改写;failInitialization 在关闭前清空 bridgeRef;App 的 catch 路径经 shouldAttemptReconnect 修复连接后抛出脱敏错误且不重放(mcp-tool.ts:777-792);SDK 的 App 调用超时为 310_000 毫秒(DaemonClient.ts:6531);App 审批按会话准入上限 min(8, max(0, maxPendingPerSession - 1))(bridgeClient.ts:1013-1018)。
R4-1 … R4-6 远程/不可达的隔离来源经 resolveMcpAppSandboxUrl 加 10 秒有限重试进入 data 文档路径;30 秒进度心跳使用声明的 progressToken;实时队列降级以非空队列为前提(eventBus.ts:1224-1226);两份参考文档已描述订阅者侧的有损载荷;beginClose() 不再中止 App 调用(只有 dispose() 会——Session.ts:4492);回放窗口先按从旧到新降级再驱逐,只有在驱逐仍无法满足预算时才降级最新段(compactionEngine.ts:909-936)。
R3-1 / R3-2 基础设计文档(中英双语)说明两层 frame 都授予 allow-same-origin,并由不可篡改的 HTTP CSP 强制执行;fallbackText 为空时 dropMcpAppHtml 会代入可见的省略说明(transcript-page-table.ts:126-149)。
B1 / F1 / F2(维护者第 2–4 轮) 两个沙箱响应都以 HTTP 头下发 Content-Security-Policy: sandbox …(mcp-app-sandbox.ts:197,256);全页共享的 App 调用名额上限 2 个加共享队列,保证审批请求不被卡死(McpApp.tsx:124-159);仅在 App「修复但不重放」路径跳过调用守卫(mcp-tool.ts:586)。维护者已在第 3、5、6 轮独立复验这三项。

R1-1(App HTML 聚合预算)—— 已在 HEAD 逐环节追踪,未确认存在绕过

对仍标记「未解决,请确认」的条目按当前代码逐环节复核:

  • 页面组装(history-replay-page.ts 的 replayContext.sendUpdate):溢出处理从最旧开始清空 App html,使用带符号字节差(serializedUpdateBytes -= old − new),因此当替换反而变大(极短 html 换成更长的省略说明)时,计入的字节数会增大而不是被掩盖。循环以页面长度为界;若连当前投递的更新也降级后仍超预算,则抛出 HistoryReplayLimitError —— 是显式失败,而不是静默放过超预算页面。
  • 封套兜底(degradeReplayEnvelopeAppHtml):同样的带符号记账与有界循环,且两个调用点(acpAgent.ts:5924、acpAgent.ts:6100)随后都立即重新执行 validateLoadReplayEnvelope。
  • 回放窗口(compactionEngine.ts):两阶段降级、带符号差值、以驱逐兜底;仅当驱逐仍无法装下时才降级最新段。
  • 实时队列(eventBus.ts):仅在非空积压将溢出时才降级;fallbackText 为空的帧永远不会被降级成空内容。
  • 历史页面(transcript-page-table.ts):fallback 为空时整体丢弃 html 并替换为省略说明。

各环节均未发现静默放行超预算、无界循环或空内容帧;无法充分降级的文档会以显式可见的错误失败。这与作者第 11 轮的复核结论一致。第 6/8 轮留下的两条相关 fails-closed 加固记录(最新段的驱逐顺序、不收缩时的循环退出)属于本窗口的延后审计记录,本轮未触碰。

维护者事项

  • 跨标签页连接耗尽(维护者第 5–6 轮)—— 维护者决策,保持开放。 每页上限已修复单标签页场景(维护者已验证),但浏览器按主机共享的 6 连接预算跨标签页生效,3 个标签页各挂 1 个 App 调用仍可能饿死审批请求。维护者明确说这是他的决定:按现状合入并把事件流交付(第 4 轮方案 (a))作为后续跟踪,或要求合入前完成。本轮不替他选择有争议的结构性方向,问题留给 @wenshao。
  • 维护者点名的低成本后续项 —— 本轮不处理,理由已记录。 F-3(两处 __HOST_ORIGIN__ 替换点的 String.replace $ 模式损坏,mcp-app-sandbox.ts:199,262,修法是改用替换函数)、F-2(256 条注册上限处改为拒绝新铸造而不是淘汰旧条目)、App 调用计数在线视图与回放不一致。维护者已把三项归类为不阻塞合入的后续工作,且本窗口仅处理 Critical;在一个已可合入的 PR 上为非 Critical 项扩大 diff 违背收敛策略。这些条目继续留待后续处理。
  • 远程 HTTPS 完整部署验收 仍是 PR 描述中由作者记录的限制;本窗口没有新的证据或要求出现。

延后的非 Critical 反馈

本窗口「Deferred non-Critical feedback」一节列出的机器人 Suggestion 属于审计记录;按仅 Critical 的收敛规则,本轮未处理、未解决、未回复这些条目。

验证

  • npm run build —— 通过(使用 COREPACK_HOME=/tmp/corepack-home;本沙箱中默认 $HOME 下的 corepack 缓存不可写)。
  • vitest run src/tools/mcp-tool.test.ts(packages/core)—— 172 通过。
  • vitest run client/components/messages/McpApp.dom.test.tsx client/components/messages/McpApp.test.ts(packages/web-shell)—— 38 通过。
  • vitest run src/serve/mcp-app-sandbox.test.ts src/serve/server/telemetry.test.ts src/serve/server/telemetry-catalog.test.ts(packages/cli)—— 88 通过。
  • vitest run src/acp-integration/session/history-replay-page.test.ts(packages/cli)—— 33 通过。
  • vitest run src/eventBus.test.ts src/compactionEngine.test.ts(packages/acp-bridge)—— 200 通过。
  • vitest run src/bridgeClient.test.ts(packages/acp-bridge)—— 149 通过。
  • vitest run test/unit/DaemonClient.test.ts(packages/sdk-typescript)—— 489 通过。
  • npm run typecheck —— 通过(完整工作区,代码树未改动)。

合计:在未改动的 head 上 1,169 项聚焦测试全部通过。未修改任何源码或测试文件;git status 干净。

Deferred non-Critical feedback

Critical-only mode is active: the round counter reached 5 (this window was seeded at round 10 by @qwen-code /takeover from 10, plus 0 change-producing round(s) since). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:轮次计数已达 5(本窗口由 @qwen-code /takeover from 10 从第 10 轮起算,此后又完成 0 个产生改动的轮次)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。

🧵 Resolved all 17 selected review thread(s). · 已关闭全部选中的 17 条评审线程。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.6

@wenshao

wenshao commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification, round 7 (head 45c260aad1)

Verdict: both new fixes work in a real browser, and nothing regressed. No new blocker. The only open decision is still the cross-tab wedge from rounds 5–6.

  • R8-1 fixed. Timeout warnings used to point at mcpServers.<srv>.timeout, which cannot raise the App deadline. They now point at appResourceTimeoutMs. Following the new advice renders the App. The deadlines themselves did not change.
  • R8-3 fixed. I injected a failed tool-result handoff. In the build without the fix, the card showed its fallback text, but the hidden App stayed alive. Its own tool call reached the approval panel and ran on the server. With the fix, the iframe is unloaded, no approval appears, and the server runs 0 calls. I found no natural trigger for this failure, so treat it as a defensive fix.
  • R8-2 fixed. The build passes. For the first time, an App call from an older turn was checked in a real browser, on the historical page, and it works.
  • Round 4–6 results still hold. 5 concurrent App calls: 5/5. MCP crash: 1 restart, no replay. The canary secret does not leak. Data-mode fallback works in Chromium and WebKit.
  • Still open, unchanged: the cross-tab wedge (3 tabs × 1 App call: 0/3 executed, 300 s timeout). Triage F-2/F-3 are also unchanged, because mcp-app-sandbox.ts has not changed since round 6.

What changed since round 6

Round 6 tested e6c7a795d0. Since then there are two fix commits and three merges of main: 0dcbf8b12a, 90b0bbd41b and 45c260aad1.

  • 52 files are touched only by this PR. The only changes in them are 0d6eb9e2db's 4 files: mcp-tool.ts, McpApp.tsx and their tests.
  • 21 files are also changed by main. I compared the PR's own ± lines in them between round 6 and now, ignoring whitespace. Two differences remain:
    • tool-registry.ts (the conflict /resolve fixed): only a comment moved. discoverToolsForServer now calls removeMcpToolsByServer. That function does the same deletions as main's inline loop, and also clears mcpAppTools. Main's fix(core): keep the deferred-tool bridge halves on the same tool #12539 rule is kept: reveal state is dropped, and the reviewed-declaration record is not touched.
    • ChatPane.tsx: main's feat(web-shell): support selective host artifact integration #12588 added the same sourceSessionId={connection.sessionId} prop the PR already had. The merge 90b0bbd41b left two copies, which fails typecheck (TS17001). cab293b3dd removed one. Both copies had the same value, so behavior does not change.

Setup

  • Two builds of the same tree (pnpm install --frozen-lockfile, pnpm run build, pnpm run bundle):
    • after: head 45c260aad1.
    • before: the same head with only 0d6eb9e2db's two source files reverted, so the source trees differ only in those files. In the bundles, the core ceiling branch appears 1 time vs 0, and the web-shell oncalltool=void 0 appears 2 times vs 0.
  • Browser and chain: Chromium 149 (Playwright 1.61.1) on macOS arm64. Browser → bundled WebShell → dist/cli.js serve on 127.0.0.1 → ACP child → a real stdio MCP server built on the official @modelcontextprotocol/ext-apps App SDK.
  • Scripted parts: only the model's tool choice. Every approval was a real click in the approval panel.
  • Rig: the round-5 rig, plus two new App variants:
    • a slow resource whose resources/read takes 12 s;
    • a self-calling App that calls its App-visible tool stable_app_tool on its own 5 s after connecting.
  • Scripts, per-run result.json files and the mutation results are under pr-12258-round7/.

1. R8-1: the timeout warning now names the setting that can change the deadline

Each server points at the same slow resource. Only its config differs.

Server config Actual deadline before (warning names) after (warning names)
timeout unset (600 000 ms default) 10 000 ms mcpServers.fxdefault.timeout ✗ mcpServers.fxdefault.appResourceTimeoutMs ✓
timeout: 120000 (operator already raised it) 10 000 ms mcpServers.fxgen.timeout ✗ (same advice again) mcpServers.fxgen.appResourceTimeoutMs ✓
timeout: 5000 (control) 5 000 ms …fxshort.timeout …fxshort.timeout (same)
appResourceTimeoutMs: 5000 (control) 5 000 ms …fxappshort.appResourceTimeoutMs same
appResourceTimeoutMs: 20000 (the new advice) 20 000 ms App renders, read done at 12.0 s App renders, read done at 12.0 s

The fixture's ledger shows the server-side abort in each case. Reads aborted at 10 000–10 003 ms or 5 001 ms, and both builds give the same numbers. So only the text of the warning changed.

R8-1

2. R8-3: a failed handoff now tears the App down (fault injection)

Can this happen naturally? The host sends sandbox-resource-ready, tool-input and tool-result as one-way postMessage notifications, and their payloads are JSON. A handler that throws inside the App cannot reject them on the host side. So I found no way to make them fail in a real run.

Injection: I patched the served WebShell bundle so that the host's PostMessageTransport.send throws for ui/notifications/tool-result. This is the "transport errors" trigger named in R8-3. Both builds got exactly the same patch.

control (after, no injection) before + injection after + injection
Card App renders text fallback, iframe display:none, src kept text fallback, iframe src removed
App document alive still alive (__ready=true, vendor frame loaded) about:blank, gone
App's own stable_app_tool call after 5 s approval shown approval shown for an App the user was told had failed no approval within 15 s
Server executions after approval 1 1 0 (0 /mcp-app/tools/call requests)

R8-3

Not checked in a browser: the "stale failure after a remount" case. The unit tests cover it, and mutation M3 below confirms the test catches it.

3. R8-2 and the historical page

After the App turn, I added 12 turns of 900 KB each and reloaded. The App turn fell outside the live window (0 live cards). Opening turn 2 from the session timeline gave the historical viewport. There:

  • the App rendered;
  • the host advertised serverTools;
  • stable_app_tool asked for approval and ran once;
  • the historical page showed 0 feedback buttons.

Round 4 could only check this path with unit tests.

historical page

4. Round 4–6 cases on the new head

Case Round 6 (e6c7a795d0) Round 7 (45c260aad1)
5 concurrent App calls in one tab, one approval every ~8 s 5/5 5/5. First result at 3 s, last at 36 s.
Kill the MCP server, then make App calls 1st fails, restart once, then ok Same. The 1st fails (MCP App tool call failed.). The server restarts once. The next stable and token calls succeed. stable_app_tool ran 3× for 3 successes, so nothing was replayed.
App-only tools in the next model turn excluded Excluded.
Canary SECRET-FIXTURE outside the App none None. 0 hits in model requests, session files, runtime and daemon logs. The only hit is the fixture's own --secret argument in settings.json.
Isolation probes and attacks blocked Blocked. The App gets a unique <uuid>.localhost origin. It cannot read the top document or storage. Daemon API and WebSocket requests fail. A consumed sandbox URL returns 404. Top-level navigation is blocked, and popups return null.
Forwarded single port (--block-isolated) not re-run Chromium and WebKit: switch to mode=data after about 10 s. The App renders, the token call works, and the vendor frame keeps its own origin.
3 tabs × 1 App call 0/3, 300 s 0/3, 300 s. 183 approval clicks, 0 executions. At 6 s, 3 session event streams and 3 POST …/mcp-app/tools/call are still open.

5. Tests and mutation

  • Focused suites on head: 861/861 passing.
    • core mcp-tool, tool-registry: 251
    • web-shell McpApp: 38
    • web-shell TranscriptViewport.mcp-app, ChatPane: 175
    • CLI mcp-app-sandbox, history-replay-page: 48
    • acp-bridge eventBus, compactionEngine, bridgeClient: 349
  • CI on 45c260aad1 is green.
  • Mutations on 0d6eb9e2db's code: 6 of 10 were killed.
Mutation Result
M1: drop the ceiling branch killed (4 tests)
M2: invert the ceiling check killed (6)
M3: remove the generation guard in failInitialization killed (1)
M4: keep oncalltool after a failure killed (3)
M6: tool-input/result catch goes back to setError killed (3)
M8: sandbox-resource catch goes back to setError killed (1)
M5: no clearTimeout in failInitialization survived; practically equivalent, because the timer callbacks already check active
M7: no active check before sendToolResult survived; practically equivalent, because a late send only reaches the guarded failInitialization
M9: connect() catch goes back to setError survived; test gap
M10: normal cleanup keeps oncalltool survived; test gap

M9 and M10 are cheap follow-up tests. Neither blocks the merge: a connect() failure is rare, and cleanup also aborts and closes the bridge.

Not covered this round

  • No real vendor run (Tableau Cloud or Amplitude) and no remote HTTPS run.
  • WebKit was only re-run for the data-mode case.
  • Triage F-2/F-3 and the live-vs-replay App-call count mismatch were not re-run. Their code is unchanged since round 6, and they remain follow-ups.

Merge reference

  • Nothing new blocks the merge. R8-1, R8-2 and R8-3 are fixed as described. The merges of main changed none of the PR's behavior.
  • The open decision is the same as rounds 5–6: the cross-tab wedge. One option is to merge and track event-stream delivery (round-4 option (a)) as a follow-up. The other is to require it before merge.
  • Cheap follow-ups: tests for M9/M10, the F-3 replacer function and the F-2 refuse-when-full rule.
  • The review decision is still CHANGES_REQUESTED, from the bot's earlier review.

Earlier rounds: round 5, round 6.

中文版

维护者验证,第 7 轮(head 45c260aad1)

结论:两个新修复在真实浏览器里都按描述生效,也没有回归。没有新的阻塞项。唯一的开放决策仍是第 5–6 轮的跨标签页卡死。

  • R8-1 已修复。 超时警告原来让人去改 mcpServers.<srv>.timeout,但这个设置抬不高 App 的期限。现在改为指向 appResourceTimeoutMs,照着新提示设置后 App 能渲染出来。期限本身没有变化。
  • R8-3 已修复。 我注入了一次 tool-result 交接失败。在没有修复的构建里,卡片显示了回退文本,但隐藏的 App 仍然存活:它自己发起的工具调用弹出了审批,并在服务器上执行了。有修复时,iframe 被卸载,不出审批,服务器执行 0 次。我没有找到自然触发这个失败的方式,所以请把它看作防御性修复。
  • R8-2 已修复。 构建通过。另外首次在真实浏览器里验证了从较早轮次发起的 App 调用,在 historical 页面上正常工作。
  • 第 4–6 轮结论仍成立。 5 个并发 App 调用 5/5;MCP 崩溃后重启 1 次、不重放;金丝雀密钥没有泄露;data 模式回退在 Chromium 和 WebKit 上都正常。
  • 仍开放,没有变化: 跨标签页卡死(3 个标签页各 1 个 App 调用:执行 0/3,300 秒超时)。triage F-2/F-3 也没变,因为 mcp-app-sandbox.ts 自第 6 轮以来没有改动。

自第 6 轮以来的变化

第 6 轮测的是 e6c7a795d0。此后有两个修复提交,以及三次合并 main:0dcbf8b12a、90b0bbd41b、45c260aad1。

  • 只由本 PR 改动的文件有 52 个。 其中的改动只有 0d6eb9e2db 的 4 个文件:mcp-tool.ts、McpApp.tsx 以及各自的测试。
  • main 也改过的文件有 21 个。 我在忽略空白的前提下,对比了这些文件里 PR 自己的 ± 行在第 6 轮和现在的差别。只剩两处:
    • tool-registry.ts(/resolve 解决的冲突):只是一段注释挪了位置。discoverToolsForServer 现在调用 removeMcpToolsByServer。这个函数做的删除和 main 的内联循环相同,另外还会清理 mcpAppTools。main fix(core): keep the deferred-tool bridge halves on the same tool #12539 的规则保持不变:清掉 reveal 状态,不动 reviewed-declaration 记录。
    • ChatPane.tsx:main 的 feat(web-shell): support selective host artifact integration #12588 加了和 PR 相同的 sourceSessionId={connection.sessionId} 属性。合并 90b0bbd41b 留下了两份,导致 typecheck 失败(TS17001)。cab293b3dd 删掉了其中一份。两份的值相同,行为没有变化。

环境

  • 同一棵树的两个构建(pnpm install --frozen-lockfile、pnpm run build、pnpm run bundle):
    • 修复后: head 45c260aad1。
    • 修复前: 同一个 head,只回退 0d6eb9e2db 的两个源文件,所以两棵源码树只在这两个文件上不同。bundle 里 core 的上限分支出现 1 次对 0 次,web-shell 的 oncalltool=void 0 出现 2 次对 0 次。
  • 浏览器和链路: macOS arm64 上的 Chromium 149(Playwright 1.61.1)。浏览器 → 打包的 WebShell → 127.0.0.1 上的 dist/cli.js serve → ACP 子进程 → 基于官方 @modelcontextprotocol/ext-apps App SDK 的真实 stdio MCP 服务器。
  • 脚本化的部分: 只有模型选哪个工具。每次审批都是在审批面板里真实点击。
  • 装置: 沿用第 5 轮的装置,新增两个 App 变体:
    • 慢资源:resources/read 耗时 12 秒;
    • 自调用 App:连接 5 秒后,自己调用 App 可见工具 stable_app_tool。
  • 脚本、每次运行的 result.json 和变异结果都在 pr-12258-round7/。

1. R8-1:超时警告现在指向能改变期限的设置

每个服务器都指向同一个慢资源,只有配置不同。

服务器配置 实际期限 修复前(警告指向) 修复后(警告指向)
未设 timeout(默认 600 000 ms) 10 000 ms mcpServers.fxdefault.timeout ✗ mcpServers.fxdefault.appResourceTimeoutMs ✓
timeout: 120000(操作者已经调高过) 10 000 ms mcpServers.fxgen.timeout ✗(重复同一条建议) mcpServers.fxgen.appResourceTimeoutMs ✓
timeout: 5000(对照) 5 000 ms …fxshort.timeout …fxshort.timeout(相同)
appResourceTimeoutMs: 5000(对照) 5 000 ms …fxappshort.appResourceTimeoutMs 相同
appResourceTimeoutMs: 20000(按新建议) 20 000 ms App 渲染,12.0 秒读完 App 渲染,12.0 秒读完

fixture 的台账记下了每种情况下服务器端的中止时间:10 000–10 003 ms 或 5 001 ms,两个构建的数字相同。所以变化的只有警告的文字。

2. R8-3:交接失败后 App 现在会被拆除(故障注入)

这种失败会自然发生吗? 宿主发送的 sandbox-resource-ready、tool-input、tool-result 都是单向 postMessage 通知,载荷是 JSON。App 内部的处理函数抛异常,也不会让宿主这边的发送失败。所以我在真实运行里没有找到让它们失败的方法。

注入方式: 我修改了服务端下发的 WebShell bundle,让宿主的 PostMessageTransport.send 在发送 ui/notifications/tool-result 时抛错。这正是 R8-3 里说的「传输出错」触发条件。两个构建打的是完全相同的补丁。

对照(修复后,不注入) 修复前 + 注入 修复后 + 注入
卡片 App 渲染 文本回退,iframe display:none,src 保留 文本回退,iframe src 被移除
App 文档 存活 仍然存活(__ready=true,vendor 帧已加载) about:blank,已卸载
App 在 5 秒后自己调用 stable_app_tool 弹出审批 为一个已告知用户「失败」的 App 弹出审批 15 秒内没有审批
批准后服务器执行次数 1 1 0(0 次 /mcp-app/tools/call 请求)

没有在浏览器里验证的: 「重新挂载后旧 mount 迟到的失败」这个场景。单测覆盖了它,下面的变异 M3 也确认测试能抓住。

3. R8-2 与历史页

App 轮次之后,我又加了 12 轮、每轮 900 KB 的回复,然后刷新页面。App 轮次落到了 live 窗口之外(live 卡片 0 张)。从会话时间线打开第 2 轮,进入了 historical 视图。在这个视图里:

  • App 正常渲染;
  • 宿主通告了 serverTools;
  • stable_app_tool 弹出审批,并执行了 1 次;
  • 历史页上的反馈按钮为 0。

第 4 轮时这条路径只能靠单测验证。

4. 在新 head 上重跑第 4–6 轮的用例

用例 第 6 轮(e6c7a795d0) 第 7 轮(45c260aad1)
单标签页 5 个并发 App 调用,约每 8 秒批准一次 5/5 5/5。 第一个结果在 3 秒,最后一个在 36 秒。
杀掉 MCP 服务器后再发 App 调用 第 1 次失败,重启一次,之后正常 相同。 第 1 次失败(MCP App tool call failed.),服务器重启一次,之后的 stable 和 token 调用都成功。stable_app_tool 成功 3 次、执行 3 次,没有重放。
下一个模型轮次里的 App 专用工具 排除 排除。
App 之外出现金丝雀 SECRET-FIXTURE 无 无。 模型请求、会话文件、runtime 和 daemon 日志里都是 0 次。唯一命中的是 settings.json 里 fixture 自己的 --secret 参数。
隔离探针与攻击 被拦截 被拦截。 App 拿到唯一的 <uuid>.localhost origin,读不到顶层文档和存储;daemon API 和 WebSocket 请求失败;已用过的沙箱 URL 返回 404;顶层导航被拦截,弹窗返回 null。
单端口转发(--block-isolated) 未重跑 Chromium 和 WebKit: 约 10 秒后切换到 mode=data。App 渲染,token 调用正常,vendor 帧保留自己的 origin。
3 个标签页 × 1 个 App 调用 0/3,300 秒 0/3,300 秒。 点了 183 次审批,执行 0 次。第 6 秒时仍有 3 条会话事件流和 3 个 POST …/mcp-app/tools/call 没有结束。

5. 测试与变异

  • head 上的定向测试套件:861/861 通过。
    • core mcp-tool、tool-registry:251
    • web-shell McpApp:38
    • web-shell TranscriptViewport.mcp-app、ChatPane:175
    • CLI mcp-app-sandbox、history-replay-page:48
    • acp-bridge eventBus、compactionEngine、bridgeClient:349
  • 45c260aad1 上的 CI 全绿。
  • 针对 0d6eb9e2db 代码的变异:10 个杀掉 6 个。
变异 结果
M1:删掉上限分支 被杀(4 个测试)
M2:把上限判断取反 被杀(6)
M3:去掉 failInitialization 里的 generation 守卫 被杀(1)
M4:失败后保留 oncalltool 被杀(3)
M6:tool-input/result 的 catch 改回 setError 被杀(3)
M8:sandbox-resource 的 catch 改回 setError 被杀(1)
M5:failInitialization 里不 clearTimeout 存活;实际等价,因为定时器回调本来就检查 active
M7:sendToolResult 前不检查 active 存活;实际等价,因为迟到的发送只会进入带守卫的 failInitialization
M9:connect() 的 catch 改回 setError 存活;测试盲区
M10:正常清理时保留 oncalltool 存活;测试盲区

M9 和 M10 可以作为低成本的后续测试补上。两者都不阻塞合并:connect() 失败很少见,而且清理时也会中止并关闭桥接。

本轮未覆盖

  • 没有跑真实 vendor(Tableau Cloud 或 Amplitude),也没有跑远端 HTTPS。
  • WebKit 只重跑了 data 模式这一项。
  • triage F-2/F-3,以及 App 调用计数在实时视图和回放里不一致的问题,本轮没有重跑。它们的代码自第 6 轮以来没有改动,仍作为后续项。

合并参考

  • 没有新的阻塞项。 R8-1、R8-2、R8-3 都按描述修好了。几次合并 main 没有改变 PR 的任何行为。
  • 开放决策与第 5–6 轮相同: 跨标签页卡死。一种选择是先合入,把事件流交付(第 4 轮方案 (a))作为后续跟踪;另一种是要求合入前完成。
  • 低成本后续项: M9/M10 的测试、F-3 改用替换函数、F-2 在表满时拒绝新注册。
  • 评审决定仍是 CHANGES_REQUESTED,来自机器人较早的一次评审。

@wenshao

wenshao commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round: no code change

Head reviewed: 45c260aad1 (matches origin/codex/amplitude-mcp-app-limits; working tree clean).

What the actionable feedback contains

The only item in this round's actionable sections is @wenshao's round-7 maintainer verification (issue-level comment ic:5852089456). It is a verification report, not a change request:

  • R8-1 fixed — timeout warnings now name appResourceTimeoutMs; verified in a real browser across five server configs, with server-side abort ledgers confirming only the warning text changed.
  • R8-3 fixed — under fault injection (host PostMessageTransport.send throwing for tool-result), the failed App is now torn down: iframe unloaded, no approval shown, 0 server executions, versus the pre-fix build where the hidden App stayed alive and ran a tool call after approval.
  • R8-2 fixed — an App call from an older turn was exercised for the first time in a real browser on the historical page: renders, advertises serverTools, runs once after approval, no feedback buttons.
  • Round 4–6 results still hold on the new head: 5/5 concurrent App calls, one MCP restart with no replay after a crash, no canary-secret leakage, isolation probes blocked, data-mode fallback working in Chromium and WebKit.
  • 861/861 focused tests pass; CI on 45c260aad1 is green. Mutation run on the latest fix commit killed 6 of 10 mutants; the 4 survivors were judged practically equivalent (M5, M7) or cheap non-blocking test gaps (M9, M10).
  • The two merge-conflict resolutions since round 6 (tool-registry.ts, ChatPane.tsx) were re-verified as behavior-preserving.

There are no inline comments, no review bodies, and no failed or still-red checks in this round's actionable sections.

Why no code change was made

  1. No new blocker and no new defect claim. The verification explicitly concludes "both new fixes work in a real browser, and nothing regressed. No new blocker." Nothing in the actionable feedback reproduces a correctness, security, build, or test defect against the current head, so there is no Required item to fix, and a probe would have nothing to reproduce.

  2. Critical-only mode is active. The deterministic brake has engaged (window seeded at round 10). The items the maintainer names as remaining work — tests for mutations M9/M10, the F-3 replacer function, the F-2 refuse-when-full rule — are explicitly categorized by the maintainer as "cheap follow-ups" that do not block the merge, and F-2/F-3 touch mcp-app-sandbox.ts, unchanged since round 6. Under critical-only mode these stay deferred to follow-up rather than growing this PR.

  3. The deferred ci-bot comment is an audit record. Per the critical-only rules it was excluded from this round's actionable sections, so no code, thread resolution, or reply was produced for it.

Open decision that needs a maintainer (unchanged from rounds 5–6)

The cross-tab wedge: 3 tabs × 1 App call → 0/3 executed, 300 s timeout; at 6 s, 3 session event streams and 3 POST …/mcp-app/tools/call requests remain open. This is a reproduced defect, but the maintainer has held it as an explicit merge decision for three consecutive rounds:

  • Option A: merge now and track the event-stream delivery fix (round-4 option (a)) as a follow-up.
  • Option B: require the cross-tab fix before merge.

This choice is a scope/product judgment, not an autonomous one: option (a) is a daemon event-stream delivery change in core infrastructure, and implementing it unilaterally in a critical-only round would contradict both the brake and the maintainer's framing. The recommendation from the evidence at hand is Option A — every verified behavior on the head passes, the wedge has a narrow multi-tab trigger, and the maintainer's own round-7 verdict records no new blocker — but the call belongs to the maintainer. It is left open here so it reads as an explicit question, not a verdict this round already reached.

State checks performed

  • git status — clean; branch up to date with origin/codex/amplitude-mcp-app-limits.
  • git log / git diff origin/main...HEAD --stat — confirmed head 45c260aad1 and PR footprint (73 files, +5839/−428) for context; no files were modified this round.
  • No build, typecheck, lint, or test runs were needed: no code changed.
中文说明

Autofix 本轮:无代码改动

评审的 head: 45c260aad1(与 origin/codex/amplitude-mcp-app-limits 一致;工作区干净)。

本轮可执行反馈的内容

本轮可执行区域里唯一的条目是 @wenshao 的第 7 轮维护者验证(issue 级评论 ic:5852089456)。它是一份验证报告,而不是修改请求:

  • R8-1 已修复 —— 超时警告现在指向 appResourceTimeoutMs;已在真实浏览器中覆盖五种服务器配置验证,服务器端的中止台账确认只有警告文字发生变化。
  • R8-3 已修复 —— 在故障注入下(宿主 PostMessageTransport.send 对 tool-result 抛错),失败的 App 现在会被拆除:iframe 被卸载、不弹审批、服务器执行 0 次;而修复前的构建里,隐藏的 App 仍然存活,并在审批通过后执行了一次工具调用。
  • R8-2 已修复 —— 首次在真实浏览器里验证了较早轮次发起的 App 调用在 historical 页面上正常工作:正常渲染、通告 serverTools、审批后执行一次、无反馈按钮。
  • 第 4–6 轮的结论在新 head 上仍成立: 5 个并发 App 调用 5/5;MCP 崩溃后重启一次且不重放;金丝雀密钥无泄露;隔离探针被拦截;data 模式回退在 Chromium 和 WebKit 上正常。
  • 定向测试 861/861 通过;45c260aad1 上 CI 全绿。 针对最新修复提交的变异测试杀掉 10 个变异中的 6 个;4 个存活者被判定为实际等价(M5、M7)或低成本且不阻塞合并的测试盲区(M9、M10)。
  • 自第 6 轮以来的两处合并冲突解决(tool-registry.ts、ChatPane.tsx)也被重新验证为不改变行为。

本轮可执行区域中没有内联评论、没有评审正文,也没有失败或持续失败的检查。

本轮不做代码改动的原因

  1. 没有新的阻塞项,也没有新的缺陷主张。 验证结论明确为「两个新修复在真实浏览器里都按描述生效,也没有回归。没有新的阻塞项。」可执行反馈中没有任何针对当前 head 复现的正确性、安全、构建或测试缺陷,因此没有必须处理的 Required 项,也没有可供探针复现的目标。

  2. 仅处理 Critical 的模式已激活。 确定性刹车已生效(窗口从第 10 轮起算)。维护者点名的剩余工作——M9/M10 变异的测试、F-3 替换函数、F-2 表满拒绝规则——都被维护者明确归类为不阻塞合并的「低成本后续项」,且 F-2/F-3 涉及的 mcp-app-sandbox.ts 自第 6 轮以来没有改动。在仅 Critical 模式下,这些条目继续延后到后续跟进,而不是让本 PR 继续膨胀。

  3. 被延后的 ci-bot 评论是审计记录。 按仅 Critical 规则,它已被排除在本轮可执行区域之外,因此本轮不为其改动代码、不解决线程、也不回复。

仍需维护者决策的开放问题(与第 5–6 轮相同)

跨标签页卡死: 3 个标签页各 1 个 App 调用 → 执行 0/3,300 秒超时;第 6 秒时仍有 3 条会话事件流和 3 个 POST …/mcp-app/tools/call 请求未结束。这是一个已复现的缺陷,但维护者已连续三轮将其明确作为合并决策持有:

  • 方案 A: 先合并,把事件流交付修复(第 4 轮方案 (a))作为后续跟进。
  • 方案 B: 要求在合并前完成跨标签页修复。

这个选择属于范围/产品判断,不应由自动化流程自行决定:方案 (a) 是核心基础设施中 daemon 事件流交付的改动,在仅 Critical 的轮次里单方面实施它,既违背刹车的目的,也违背维护者的表述。就现有证据而言的倾向性建议是方案 A——当前 head 上所有已验证行为均通过,卡死的触发条件局限于多标签页场景,且维护者第 7 轮的结论记录了无新阻塞项——但最终决定权在维护者。此处将其保持开放,是为了让维护者读到一个明确的问题,而不是本轮已经擅自得出的结论。

本轮执行的状态检查

  • git status —— 干净;分支与 origin/codex/amplitude-mcp-app-limits 同步。
  • git log / git diff origin/main...HEAD --stat —— 确认 head 为 45c260aad1 及 PR 的改动范围(73 个文件,+5839/−428)作为背景;本轮未修改任何文件。
  • 未运行构建、typecheck、lint 或测试:本轮没有代码改动。

Deferred non-Critical feedback

Critical-only mode is active: the round counter reached 5 (this window was seeded at round 10 by @qwen-code /takeover from 10, plus 0 change-producing round(s) since). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:轮次计数已达 5(本窗口由 @qwen-code /takeover from 10 从第 10 轮起算,此后又完成 0 个产生改动的轮次)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.6

@qwen-code-dev-bot

Copy link
Copy Markdown
Collaborator

🤖 Reviewed the latest feedback — no changes needed. Why, point by point: · 已审阅最新反馈——无需改动。逐点说明原因如下:

Autofix round — no action taken

No changes were made in this round. There was nothing actionable to address:

  • Critical-only mode is active. The workflow's deterministic brake has engaged for this PR (the counting window was seeded at round 10 by @qwen-code /takeover from 10, and the round counter has reached 5). All remaining feedback was deterministically deferred and appears only in the audit-only Deferred non-Critical feedback section of the prepared feedback, which this bot does not act on: one non-Critical review from @qwen-code-ci-bot, plus a notice that @wenshao's regular-feedback budget for this window is spent.
  • No Critical feedback. The actionable sections (reviews, inline comments, issue-level comments) contained no items.
  • No failed checks. All CI checks on the current head are green (SUCCESS) or legitimately SKIPPED/CANCELLED; there are no failed or still-red checks to diagnose.
  • No base-conflict resolution was requested (--conflict false), so no merge of origin/main was performed.

To resume automated handling of the deferred non-Critical feedback, a maintainer can start a fresh counting window by commenting @qwen-code /retry on the PR, or escalate a specific item by prefixing a comment with [Critical] or submitting a Request changes review.

Verification

No code changes were made, so no build, typecheck, lint, or test runs were required. The round's inputs were verified as follows:

  • Read the prepared feedback for this round — actionable sections (reviews, inline comments, issue-level comments, failed checks, still-red checks) are all empty; only the audit-only deferred section is present.
  • Inspected the raw CI check rollup for the current head — every check reports SUCCESS, SKIPPED, or CANCELLED (the cancelled entries are superseded route jobs); no FAILURE state exists.
  • git status --porcelain on the PR head branch is clean; the working tree was left untouched.
中文说明

Autofix 本轮 —— 未做任何改动

本轮未进行任何修改,因为没有需要处理的可执行事项:

  • 仅 Critical 模式已生效。 工作流的确定性刹车已对本 PR 触发(计数窗口由 @qwen-code /takeover from 10 从第 10 轮起算,轮次计数已达 5)。所有剩余反馈均被确定性延后,仅出现在已准备反馈的"延后的非 Critical 反馈"审计区段中,本机器人不会对其采取行动:一条来自 @qwen-code-ci-bot 的非 Critical 评审,以及 @wenshao 本窗口常规反馈预算已用完的提示。
  • 没有 Critical 反馈。 可执行区段(评审、行内评论、issue 级评论)中没有任何条目。
  • 没有失败的检查。 当前 head 上的所有 CI 检查均为绿色(SUCCESS)或合理的 SKIPPED/CANCELLED;没有需要诊断的失败或持续红色检查。
  • 未请求解决基线冲突(--conflict false),因此未执行 origin/main 的合并。

如需恢复对延后的非 Critical 反馈的自动处理,维护者可在 PR 上评论 @qwen-code /retry 开启新的计数窗口,或以 [Critical] 开头发表评论、提交 Request changes 评审来升级某个具体条目。

验证

由于未做任何代码改动,无需运行构建、类型检查、lint 或测试。本轮输入的核实方式如下:

  • 阅读了本轮准备好的反馈——可执行区段(评审、行内评论、issue 级评论、失败检查、持续红色检查)均为空;仅存在仅用于审计的延后区段。
  • 检查了当前 head 的原始 CI 检查汇总——每项检查均为 SUCCESS、SKIPPED 或 CANCELLED(被取消的条目是被取代的 route 任务);不存在 FAILURE 状态。
  • PR head 分支上的 git status --porcelain 为空;工作树保持原样未动。

Deferred non-Critical feedback

Critical-only mode is active: the round counter reached 5 (this window was seeded at round 10 by @qwen-code /takeover from 10, plus 0 change-producing round(s) since). The workflow excluded the non-Critical feedback below from this round's actionable sections; the items remain open for human follow-up. Maintainer feedback is deferred only after its author has used 2 regular feedback batches in this window's Critical-only tail; authors at that budget, if any, are named below. (@qwen-code /retry starts a fresh counting window.)

中文说明

已进入仅处理 Critical 的模式:轮次计数已达 5(本窗口由 @qwen-code /takeover from 10 从第 10 轮起算,此后又完成 0 个产生改动的轮次)。本轮可执行区域已排除下方非 Critical 反馈;这些条目保持开放,留待人工跟进。维护者反馈仅在其本人于本窗口 Critical-only 阶段已使用 2 批常规反馈预算后才会延后;达到预算的作者(如有)在下方点名。(评论 @qwen-code /retry 可开启新的计数窗口。)

Base-conflict check · 基分支冲突检查: no conflict with main. · 与 main 无冲突。


🧠 Handled by Qwen Code · model/模型 kimi-k3 · CLI 0.24.6

@wenshao

wenshao commented Sep 27, 2026

Copy link
Copy Markdown
Collaborator

@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 45c260aad

Not approving, on budget rather than on a finding I can prove. This is a 73-file, +5839/-428 change whose production surface the repository's own Stage 0 measured at 1833 lines across five packages, 394 of them in packages/core/src. An independent Critical-only pass over that surface, to the confidence bar this channel holds itself to, does not fit in the window I have; publishing an approval I cannot back with evidence would be worse than publishing this.

What I did confirm: R1-1's terminal leg is closed at this head

The last automated round at this commit carried R1-1 forward as "Unresolved, please confirm" — but it says so only because its verifier wave exceeded its wall clock (22 of 54 agents returned, so none of that round's 30 findings carries a verdict). The finding itself is worth separating from the round's inability to rule on it, because R1-1 named the restore-envelope leg as terminal: a recorded session whose replay page held eight ceiling-size App documents could not be loaded at all, history-replay-page.ts rethrew instead of degrading, acpAgent.ts mapped that to RequestError(-32012, transcript_page_too_large), and the message named neither the App documents nor appResourceMaxBytes.

That leg is closed in the code at this head. history-replay-page.ts now carries blankReplayedMcpAppHtml (copies a replayed update with its MCP App html blanked, keeping fallbackText so the client renders text in place of the App) and degradeReplayEnvelopeAppHtml (blanks oldest-first until the serialized envelope fits), and the page-assembly overflow pass does the same before it will throw — lines 293-316 blank App html oldest-first across both the retained and the delivered update, and HistoryReplayLimitError at line 321 is reached only for a page that still does not fit after every App document is degraded. The file's own comment states the invariant: it "cannot fail closed on a page whose App documents degrade cleanly". So the unit of loss is no longer the whole page for the life of the session.

I did not re-trace the other legs of R1-1 (the compacted replay window, which the finding itself already downgraded to a degrade rather than data loss because fullTranscriptAvailable: true and the frozen pagination anchor let a client re-page), nor the 16 historical Critical threads that prior posted rounds ruled fixed, nor the four other blockers the last round re-traced without publishing a verdict. All 21 Critical threads are marked resolved and all 33 still-unresolved threads are [Suggestion]; a resolved flag is not proof of a fix, and I am not treating it as one — I am saying plainly that I verified one leg myself and inherited the rest.

The one current item I verified in code: the App-call limiter is per-tab

McpApp.tsx bounds concurrent App tool calls with module scope:

// Share the limit across cards so App requests leave HTTP/1.1 connections for
// session events and approval requests. Hold the slot until the request settles.
let activeAppToolCalls = 0;
const waitingAppToolCalls = new Set<() => void>();

The mechanism is sound for what a module-scoped counter can do — the slot is acquired against an AbortSignal, held until the request settles, and handed to the next waiter on release. But a module-scoped counter bounds one JavaScript realm, which is one browser tab. The stated purpose is to leave HTTP/1.1 connections for session events and approval requests, and the browser's per-origin connection budget is shared across every tab of that origin, not per tab. N tabs each admitting up to the per-tab limit can therefore consume the very budget the limiter exists to protect, and the sessions and approvals it is protecting are the ones in the other tabs.

I am reporting this as an open item rather than as a Critical I have proven, because whether it is a defect depends on a decision the diff cannot make: whether the aggregate belongs on the daemon side (where the POST /session/:id/mcp-app/tools/call route already sits, and where a cross-client bound is enforceable) or whether per-tab is the intended contract and the comment should say so. The last automated round reached the same root cause at the same line and called it a product decision. Either answer is defensible; what is not defensible is a comment promising a guarantee the scope cannot deliver.

CI

Green at this head across everything that ran, which matters here because R6-1 — the route-catalog drift Critical — was originally evidenced by a failing unit suite: Test (ubuntu-latest, Node 22.x) passes in 22m18s, Lint & Static in 12m1s, and so do web-shell E2E Smoke, Capture web-shell visuals, Serve A/B, Integration Tests (no-AK, No Sandbox), the TUI parity and no-flicker gates, Live Host (macos-latest), the full Java matrix including Real daemon E2E and Hosted no-tool processes, and review-pr. No failure is attributable to this PR. Note for whoever merges: the CHANGES_REQUESTED decision still showing on this PR is carried by a review at the superseded cab293b3d and does not reflect this head; a maintainer has approved at 45c260aad.

What would let the next pass close

Two things, both cheap for the author and neither a request for new work:

  1. A statement of where the cross-tab App-call bound is intended to live — daemon-side, per-tab by contract, or a follow-up issue — so the limiter's comment and its scope agree.
  2. If the remaining R1-1 legs and the 16 inherited Critical rulings have already been re-traced against 45c260aad somewhere I have not read, a pointer to that; the last round explicitly declined to re-trace them under its own budget.

Verdict: COMMENT — No Critical proven at this head, and one historical terminal leg (R1-1's restore envelope) positively confirmed fixed in code. What blocks an approval is that the independent Critical-only pass over 1833 production lines across five packages could not be completed in budget, and that the App-call limiter's per-tab scope does not deliver the cross-tab guarantee its own comment states.

@wenshao
wenshao dismissed a stale review September 27, 2026 04:56

fixed

@samuelhsin
samuelhsin added this pull request to the merge queue Sep 27, 2026
Merged via the queue into main with commit 81c260b Sep 27, 2026
190 of 193 checks passed
qwen-code-dev-bot added a commit to yiliang114/qwen-code that referenced this pull request Sep 27, 2026
…gate

Both sides edited the same `if (webShellDir)` block in createServeApp: this
branch passes the desktop-relay CSP flag to mountWebShellAssets, while main
(QwenLM#12258) rewired mountMcpAppSandbox to take an origin-allow callback and
return a disposer that run-qwen-serve stops on shutdown. The two changes are
adjacent, not overlapping, so keep both: the 4-arg mountWebShellAssets call
and main's sandbox wiring with its app.locals capture.
@samuelhsin

Copy link
Copy Markdown
Collaborator Author

R12 recheck: this PR was merged at 2026-09-27 04:56:49 UTC as 81c260b. The final PR head is 45c260a.

I checked the latest reviews and verification reports, the final CI rollup, and the four files from 0d6eb9e against the final head. Those four files are unchanged. CI has 33 successful checks, 165 skipped checks and two cancelled superseded route jobs, with no failed or pending check. R8-1 and R8-3 have explicit fixed rulings; the maintainer's round-7 report independently confirms both fixes in a real browser, including that failed Apps no longer execute tool calls. The latest sandbox round reports 105/105 executed assertions and no new blocking finding.

No code change or new test run was needed in this recheck. This does not close the known cross-tab HTTP/1.1 wedge: the current limiter is per page, and cross-tab-safe delivery remains follow-up work. The event-stream proposal should preserve client-targeted private App results and cancellation. The previously deferred Low findings F-1/F-2/F-3/F-5 also remain open; merge and green CI do not mean those have been fixed. Earlier vendor/remote evidence retains its original scope.

tonydzi pushed a commit to tonydzi/qwen-code that referenced this pull request Oct 7, 2026
When no `appResourceTimeoutMs` is configured, the App resource read deadline
derives from the server `timeout` through
`boundedAppLimit(mcpTimeout, DEFAULT, 1, DEFAULT)`, so a `timeout` at or above
`MCP_APP_RESOURCE_TIMEOUT_DEFAULT_MS` (10 s) yields exactly 10 s. The timeout
warning named only `appResourceTimeoutMs` in that case, which hides both which
key produced the limit and why raising `timeout` changed nothing, and points at
a key the operator never set. `docs/users/features/mcp.md` already documents the
real model ("the deadline remains the smaller of the general `timeout` and
10,000 ms"), so the diagnostic contradicted the repository's own docs.

The warning now names `timeout` as the source plus the cap that pinned it and
the one key that can go past it. Below the cap nothing changes: the cap is not
binding there, raising `timeout` does lift the deadline, and naming the cap
would send the operator the wrong way. An explicit `appResourceTimeoutMs` still
owns the deadline and is still named alone.

Deferred review finding ic:5746879970 from QwenLM#12258.

Authored by Mycroft, the synthetic co-founder at Anton Dzyatkovsky's lab
(autonomous mode; named responsible person: Anton Dziatkovskii). The test runs
above were independently re-executed before submission.

Assisted-by: Claude Code / claude-opus-5
Machine: MacBook-Anton
Account: tonydzi
Operator: Anton Dziatkovskii
Signed-off-by: tonydzi <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

autofix/takeover Summon the autofix loop to manage this PR (remove to release; needs triage+)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

6 participants