Skip to content

feat(mobile): restore scoped connections after Activity recreation - #12247

Merged
wenshao merged 11 commits into
QwenLM:mainfrom
jabrailkhalil:feat/android-phase2-recovery
Oct 5, 2026
Merged

wenshao merged 11 commits into
QwenLM:mainfrom
jabrailkhalil:feat/android-phase2-recovery

Conversation

@jabrailkhalil

@jabrailkhalil jabrailkhalil commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor

What this PR does

Restores the selected text-only connection and validated session/workspace route in a fresh WebView after Activity recreation, and preserves an explicit retry screen after renderer or connection failure. This revision merges current main while retaining both recovery state and the accepted file-picker result reservation.

Current head: f521dfd7d8c831240180b1231acdd686c1088bd0. Based on main 1a5aae80d9; #11722, #12121 and #12126 are merged. This remains an independently reviewable Phase 2 slice.

Why it's needed

Activity recreation previously returned to the connection list and lost navigation context. Recovery must reread the encrypted profile store rather than persist credentials or revive stale native callbacks.

Reviewer Test Plan

How to verify

Recreate an active connection and confirm its supported session/workspace route resumes in a fresh WebView. Recreate a retry screen and confirm it still requires explicit retry. Deleting or changing the profile must invalidate restoration. Inspect saved state for absence of credentials and arbitrary URLs. Open the file picker, recreate, and return its old result: it must never attach to the replacement document; a later file operation should work.

Evidence (Before & After)

The current integration preserves the recovery route/history hooks and file-picker navigation, error, destruction and saved-result hooks. Local JVM tests and APK/lint checks pass. Prior device evidence in the PR remains tied to its original commits; no device run on this integration is claimed. The previous Ubuntu test failure was a Vitest worker/RPC timeout after 33,380 passed tests, not an assertion failure; this revision needs fresh upstream CI and the already-requested maintainer triage of the precheck flag.

Tested on

Windows: debug, unsigned release and instrumentation APKs compile; 25 JVM tests and Android lint pass on this revision. No new local emulator/physical-device run, macOS or Linux execution is claimed.

Current-head upstream CI and any device checks are reported separately in GitHub; previous-head green checks are not evidence for this revision.

Current-head upstream CI update (September 29): native build/unit/lint and Android API 26/36 passed, together with Lint & Static, no-AK integration, desktop-shell checks and Web Shell smoke. The Ubuntu core suite has one failed cold-scan latency assertion and 33,155 passing tests. This is a real assertion failure, distinct from the old RPC timeout; the core tree matches main. Author-side rerun was denied for lack of upstream rights. Exact logs and maintainer rerun request.

Environment

Windows, JDK 17, Gradle 8.2.1, compile SDK 34. Native checks: assembleDebug assembleRelease assembleDebugAndroidTest testDebugUnitTest lintDebug.

Risk & Scope

Unsent text and pending native operations are not resumed; a cold launch without saved state starts at Connections. Real devices, production credentials and combined microphone/download behavior remain separate acceptance work. A precheck flag on negative security fixtures requires maintainer review; this PR does not bypass that gate.

Design: English / 简体中文.

Linked Issues

Refs #11704, #11722, #12121, #12126.

中文说明

本 PR 的改动

Activity 重建后在新的 WebView 恢复选定纯文本连接及验证过的会话/工作区路由;渲染器或连接失败后保留明确的重试页。本次合并当前 main,同时保留恢复状态和已接受文件选择器的结果占用状态。

当前提交:f521dfd7d8c831240180b1231acdd686c1088bd0。基于 main 1a5aae80d9;#11722、#12121 和 #12126 已合并。这仍是可独立审阅的 Phase 2 切片。

为什么需要

此前 Activity 重建会返回连接列表并丢失导航上下文。恢复必须重新读取加密配置存储,不得持久化凭据或恢复过期原生回调。

审阅者测试计划

验证方法

重建活动连接,确认在新的 WebView 恢复支持的会话/工作区路由。重建重试页仍应要求明确重试。删除或更改配置应使恢复失效。检查保存状态不含凭据及任意 URL。打开文件选择器后重建并返回旧结果,旧结果不得关联新文档;后续文件操作应可用。

前后证据

当前集成保留恢复路由/历史钩子,以及文件选择器的导航、错误、销毁和结果保存钩子。本地 JVM、APK 构建和 lint 通过。PR 先前设备证据仍仅对应各自提交,不声称在本次集成运行过设备测试。旧 Ubuntu 失败发生于 33,380 项通过后的 Vitest worker/RPC 超时,并非断言失败;本次需要新的上游 CI,以及已请求的维护者 precheck 人工分流。

测试平台

Windows:本次 debug、未签名 release 和 instrumentation APK 编译成功;25 项 JVM 测试及 Android lint 通过。不声称完成新的本地模拟器/真机、macOS 或 Linux 运行。

当前提交的上游 CI 和设备检查由 GitHub 单独报告;前一提交的绿色检查不作为本次证据。

9月29日当前提交上游 CI:原生构建/单测/lint、Android API26/36、静态检查、no-AK 集成、桌面和 Web Shell smoke 均通过。Ubuntu core 套件一项冷扫描延迟断言失败,33,155项通过;这是实际断言失败,与旧 RPC 超时不同,core 子树与 main 相同。作者因缺少上游权限无法重跑。精确日志及维护者重跑请求。

环境

Windows、JDK17、Gradle8.2.1、compile SDK34。原生检查:assembleDebug assembleRelease assembleDebugAndroidTest testDebugUnitTest lintDebug。

风险与范围

不恢复未发送文本和待处理原生操作;没有保存状态的冷启动进入连接列表。真机、生产凭据及组合麦克风/下载行为另行验收。负面安全夹具上的 precheck 标记需要维护者审阅,本 PR 不绕过该门槛。

设计:English / 简体中文。

关联问题

关联 #11704、#11722、#12121、#12126。

October 3 integration status

Current main 1a5aae80d9 is integrated after #12127 merged. Microphone-authorized connections are excluded from saved automatic recovery and return to Connections on recreation; text-only recovery and explicit Retry remain. A regression uses a persisted fixture profile and real WebView setup to check absent recovery state and Connections after recreation. README and both design languages match. On Windows/JDK 17/SDK 34, debug, unsigned release and instrumentation APK builds, 25 JVM tests and Android lint pass. The new device test is compiled, not executed locally; fresh upstream device/CI checks and review remain required. The sandbox's navigation/recovery coverage Suggestions are recorded in #13111, not claimed as production defects or silently marked fixed. The interrupted triage workflow is not a pass.

中文:#12127 合入后,本次集成 main 1a5aae80d9。已授权麦克风的连接不写入自动恢复状态,Activity 重建后返回 Connections;纯文本恢复及明确 Retry 保留。回归测试使用已持久化夹具配置和真实 WebView 初始化,检查恢复状态缺失及重建后 Connections。README 和双语设计一致。Windows/JDK 17/SDK 34 上 debug、未签名 release、instrumentation APK 构建、25 项 JVM 测试和 Android lint 通过。新设备测试仅编译,未在本地执行;仍需当前提交的上游设备/CI 检查及审查。沙箱报告的导航/恢复覆盖 Suggestions 记录在 #13111,不称为生产缺陷或静默标记已修复。中断的 triage 工作流不算通过。

@jabrailkhalil

jabrailkhalil commented Sep 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Native E2E verification for 05a3fb76f664dc47760ee810416c1bbe15bf27c1 (Android tree 827fa1b6567c2cd907937625a246ecb3d8eccde9, identical to tested 8e1f527e9b):

Check Result
JVM 25 passed
Android debug / unsigned release / instrumentation builds Passed, Java17 + Gradle8.2.1 on Windows
Android lint 0 errors, 6 existing dependency notices
API36 / WebView134.0.6998.135, full committed native suite 15 passed, 1 assumption skip; all 8 recovery cases passed
API26 / WebView69.0.3497.100, full committed native suite 5 passed, 11 capability skips
Independent unchanged before/after reproduction Failed on combined baseline de5a05426a, passed against this implementation
Desktop isolation Passed

Modern instrumentation used -e requireProfileIsolation true. Counts distinguish actual passes from assumption skips. Tests exercised real ActivityScenario.recreate(), real WebView renderer termination, explicit Retry surviving recreation, current vault credentials on the new page, same-origin profile isolation, profile rename, deletion, token/origin changes, saved-state contents and invalid identifiers. A separate real legacy UI check confirmed Connect shows the provider-update screen without creating a WebView.

Reproduction command from packages/mobile-shell after installing the debug and test APKs:

adb -s emulator-5560 shell am instrument -w -r -e requireProfileIsolation true -e class com.qwen.mobileshell.ConnectionRecoveryDeviceTest com.qwen.mobileshell.test/androidx.test.runner.AndroidJUnitRunner

Debug APK SHA-256: 293060a3ca6ebdad69a952fd4d70443192eea504aac223a18e6c3a13e2379dc5.

The initial verification found an invalid test setup that reinserted a retired browser ID. It was corrected into independent deletion/origin-change fixtures; the single synthetic overlap was repaired with an encrypted backup while preserving other profiles. No app-data reset or product bypass was used. Final results above are from corrected fixtures; the independent baseline reproduction remained unchanged. Two clean self-audit passes and a separate code review found no actionable issue in the final commit.

Scope: native fixtures, not production daemon/H5 or a real voice provider. Actual OS process-kill/relaunch, physical-device and TalkBack acceptance are not claimed. Unsent drafts and native operations are intentionally not restored. The global CLI cannot exercise this Android feature. Full Phase 2 still depends on per-device server credentials/revocation and background-notification work; this PR does not claim to complete those.

中文:此提交在 Windows/Java17/Gradle8.2.1 下构建通过,JVM 25 项通过,lint 0 错误、6 项既有依赖提示。API36 完整原生套件 15 通过/1 条件跳过,包含全部 8 项恢复场景;API26 为 5 通过/11 能力跳过,另通过真实 Connect 界面验证旧提供程序拒绝创建 WebView。独立基线测试在旧实现失败,在本实现不修改测试即可通过。验证了真实 Activity 重建、真实渲染进程终止、重建后仍需明确 Retry、从当前存储重新注入凭据、同源配置隔离及重命名/删除/地址和令牌轮换。初次测试错误地重用已退役浏览器 ID,现已拆分独立夹具;仅修复单个测试条目并保留加密备份与其他配置,没有清空应用数据或绕过产品检查。最终提交经过两轮干净自审及独立审查。以上不代表生产 H5、真实语音服务、系统杀进程后恢复、真机或 TalkBack 验收;未发送草稿和原生操作不恢复。逐设备服务端凭据撤销与后台通知仍是独立 Phase 2 工作。

Final test-only follow-up 8e1f527e9b fixes a separately reproduced idle-preconnect failure in the loopback fixture and adds a liveness regression; the same independent diagnostic and all 16 native cases (passes/skips as above) were rerun. The earlier combined rename timeout had no captured socket cause, so its connection to this mechanism remains an inference. Product recovery code is unchanged. CI requested a newer lint gate from main; normal merge b22d8a8b66 incorporates upstream 9e6d058b41 with the exact tested Android tree preserved. No force push or gate bypass. The new CI run is pending.

中文补充:最终测试提交修复独立复现的空闲预连接导致夹具停止问题,新增存活回归;相同独立诊断及全部16项原生测试已重跑。较早重命名超时没有记录当时 socket 原因,因此两者关系仍是推断,产品恢复代码未变。按 CI 要求正常合并上游 9e6d058b41 更新规则,Android 源码树与已测版本完全一致,没有强推或绕过检查;等待新 CI。

# Conflicts:
#	packages/mobile-shell/README.md
#	packages/mobile-shell/app/src/main/java/com/qwen/mobileshell/MainActivity.kt
#	packages/mobile-shell/app/src/main/res/values/strings.xml
@jabrailkhalil
jabrailkhalil marked this pull request as ready for review September 22, 2026 15:13
@jabrailkhalil

jabrailkhalil commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor Author

October 1: the current head is 5dd0e3e2924fd06c5d45679e7f070af47d3758f3, updated from main after #13094 merged. The automated precheck still explicitly requires maintainer authorization on this head. The existing negative credential fixtures assert rejection and nonserialization; no guard or fixture has been removed to evade the check. Please inspect this revision and authorize its triage/review if appropriate. Native build/unit/lint and API 26/36 checks passed; the older failures and precheck explanations below remain historical evidence tied to their stated heads.


Current-head upstream CI update (September 29): native build/unit/lint and Android API 26/36 passed, together with Lint & Static, no-AK integration, desktop-shell checks and Web Shell smoke. The Ubuntu core suite has one failed cold-scan latency assertion and 33,155 passing tests. This is a real assertion failure, distinct from the old RPC timeout; the core tree matches main. Author-side rerun was denied for lack of upstream rights. Exact logs and maintainer rerun request.

9月29日当前提交上游 CI:原生构建/单测/lint、Android API26/36、静态检查、no-AK 集成、桌面和 Web Shell smoke 均通过。Ubuntu core 套件一项冷扫描延迟断言失败,33,155项通过;这是实际断言失败,与旧 RPC 超时不同,core 子树与 main 相同。作者因缺少上游权限无法重跑。精确日志及维护者重跑请求。


September 29 update — current head 87960f49991fa408b3178e13b20993b77e96dd93.

Merged current main 9f6138ae441a and resolved the conflict by preserving both connection recovery and the now-accepted #12126 picker: separate saved-state keys, picker cancellation on navigation/destruction, and recovery on renderer/connection failure. This is a normal forward push; existing branch history is retained.

Local Windows/JDK17: debug, unsigned release and instrumentation APKs compile; 25 JVM tests and lint pass. No new local device acceptance is claimed. Current-head CI must rerun; the earlier Ubuntu RPC timeout and precheck flag described below belong to the previous head. The request for maintainer triage of the negative security-fixture precheck remains open. Please evaluate this updated integration when that gate is cleared; no bypass or false security acknowledgement has been added.

中文:已合并当前 main,保留连接恢复及 #12126 文件选择器的独立状态、导航/销毁取消和失败恢复逻辑,普通前向推送保留历史。三类 APK 编译、25 项 JVM 和 lint 通过,不声称新的本地设备验收。需当前提交 CI;下方旧 Ubuntu RPC 超时和 precheck 属于前一提交。负面安全夹具的人工 precheck 分流请求仍待维护者处理,未绕过门槛。

Earlier verification, tied to its original commits

The Ubuntu Test failure is the shared runner/Vitest RPC failure: 1,102 test files and 33,380 tests passed, with no failed assertion, followed only by [vitest-worker]: Timeout calling "onTaskUpdate". I do not have upstream workflow rerun permission; could a maintainer please rerun it?

The precheck's prompt_injection:print_secrets flag is also a false positive from negative security-test data in this recovery slice (for example token=secret): those tests assert that credentials and fragments are rejected and never serialized. The PR contains no prompt to reveal credentials. Could a maintainer also manually trigger /triage or /review for this ready head?

…-recovery

# Conflicts:
#	packages/mobile-shell/README.md
#	packages/mobile-shell/app/src/main/java/com/qwen/mobileshell/MainActivity.kt
@wenshao

wenshao commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification: PR #12247 — feat(mobile): restore scoped connections after Activity recreation

Verdict: merge-ready — 167/167 scripted assertions passed (0 unexpected failures).
Verified head: 5dd0e3e2924fd06c5d45679e7f070af47d3758f3; control base: 0a5f518b4f73fbd10ad58a764d456afc1961e9d1 (the PR's baseRefOid).

中文摘要
  • 结论:merge-ready。167/167 条脚本断言通过,无意外失败。
  • A/B 证明(中心结论):同一台 API 36 模拟器(WebView 133)、同一脚本驱动——base 在进程死亡式重建后回到 Connections 列表(缺陷签名复现),head 在新 WebView 中恢复了同一条 /session/ab-cd_12?workspace=wk_34&context=live 路由并带上了从加密 vault 重新读取的 token;force-stop 冷启动不误恢复(回到 Connections)。
  • 设备套件:head 全量 33 项(含 9 项新增恢复测试)通过 32、跳过 1(能力假设跳过,base 同样跳过);base 全量 24 项通过 23、跳过 1。变异矩阵后 pristine 控制行再次全绿。
  • 变异矩阵 11 行:9 行预期击杀全部击杀(含同文件正向对照),2 行预期幸存均为覆盖缺口(8192 URL 长度边界无钉扎;连接错误后的重试路由无钉扎),不构成合并阻塞。
  • 构建/静态门:两臂 assembleDebug/assembleRelease/assembleDebugAndroidTest/testDebugUnitTest/lintDebug 全绿;lint 活性用植入 NewApi 违规验证(如期失败);Gradle wrapper jar 与仓库内钉住的 SHA-256 一致,两臂相同。
  • precheck 的 prompt_injection 标记:已逐行通读全部 607 行 diff(含 e2e 计划与设计文档),未发现针对模型的注入指令;该标记的触发源大概率为 e2e 计划中的祈使句文风。
  • 未覆盖:API 26 实机车道(上游 CI 已在该 head 通过,本地未重跑);物理设备、TalkBack、生产凭据。

Central claim and A/B proof

Claim: after Android recreates the Activity, the app restores the selected connection and its validated session/workspace route in a fresh WebView, with credentials re-read from the encrypted vault; a cold launch without saved state must not restore.

Method: one host-driven scenario executed identically against both builds on the same emulator — add a loopback profile (origin http://127.0.0.1:18080, synthetic token), connect, history.replaceState to /session/ab-cd_12?workspace=wk_34&context=live via CDP, then Home → am kill (task and saved instance state survive, process dies) → relaunch. This is the saved-instance-state restore path that onCreate(bundle) serves, the same path ActivityScenario.recreate() exercises in the committed device tests.

Cell Before recreation After recreation
base 0a5f518b fixture loaded, token attached, route set Connections list, no WebView — connection lost (the bug this PR fixes)
head 5dd0e3e2 fixture loaded, token attached, route set fresh WebView, same route, fresh vault token attached
head cold-launch control — am force-stop + relaunch → Connections list (no spurious restore)

Witness: 06-ab-terminal-log.png; device screens: 01-base-after-recreation-connections.png vs 02-head-after-recreation-restored.png, control 03-head-cold-launch-connections.png.

Gates (both arms, author's own command set)

./gradlew --no-daemon :app:assembleDebug :app:assembleRelease :app:testDebugUnitTest :app:lintDebug :app:assembleDebugAndroidTest

Gate base head
Build (debug + unsigned release + androidTest APK) success success
JVM unit tests 21/21 25/25 (= 21 + the 4 new ConnectionNavigationTest tests)
lintDebug 0 errors (6 warnings) 0 errors (6 warnings, identical set)
Device suite (API 36, WebView 133.0.6943.137, requireProfileIsolation=true) 23 pass / 1 capability skip (24) 32 pass / 1 capability skip (33 = 24 + the 9 new ConnectionRecoveryDeviceTest tests)

The skip on both arms is oldProviderFailsClosedBeforeCreatingNamedProfileOrWebView (pre-existing, capability-gated; this image's WebView is above the floor it tests). Lint liveness was proven by planting an API-29 call (webViewRenderProcess, minSdk 26): lintDebug failed at the planted line, then the tree was restored. The Gradle wrapper jar's SHA-256 matches the committed gradle-wrapper.jar.sha256 and is identical on both arms; the PR touches no build/launcher/CI files. Witness: 08-device-suites.png.

Vacuity: mutation matrix (11 rows, one mutation at a time, restore + clean-tree check between rows)

JVM rows gate :app:testDebugUnitTest --tests ConnectionNavigationTest; device rows rebuild both APKs and run ConnectionRecoveryDeviceTest on the emulator. Every expected kill landed on the assertion the mutation targets; the positive control (M5) proves the harness can go red; the pristine control row after the matrix is green (33/33).

Row Mutation Expected Observed Killed by / classification
M1 identifier regex allows dots killed killed untrustedOrUnsupportedNavigationFallsBackToRoot
M2 duplicate query keys allowed killed killed same
M3 arbitrary context value accepted killed killed savedFieldsAreValidatedBeforeReconstruction + untrusted…
M4 same-origin gate dropped in capture killed killed untrustedOrUnsupportedNavigationFallsBackToRoot
M5 positive control: url() drops the query killed killed retainsOnlySupportedNavigation
M6 URL cap 8192→8191 survive survived coverage gap — nothing pins the 8192 boundary
D1 restore bypassed (loadProfiles always lists) killed killed 4 recreation tests red
D2 saved-state bundle write removed killed killed 4 recreation tests red
D3 doUpdateVisitedHistory tracking removed killed killed terminatedRendererRequiresRetryEvenAfterRecreation
D4 connection-error path reverted to pre-PR semantics survive survived coverage gap — see Findings
D5 renderer death auto-reconnects instead of explicit Retry killed killed terminatedRendererRequiresRetryEvenAfterRecreation

Witness: 07-mutation-matrix.png.

Sibling-shape sweep (scratch probe, not part of the PR)

Beyond the committed fixtures, a scratch JUnit probe (ConnectionNavigationSiblingProbe.kt, since removed) drove adjacent shapes through ConnectionNavigation.capture: 128/129-char identifier boundaries, >8192-char URLs, query-key case variants (Workspace), context=LIVE, ?workspace (no =), doubled separators, percent-encoded keys, session without trailing slash, //session/…, encoded %2e%2e, non-ASCII and control characters in identifiers, ?context=live&workspace=w order swap, root-with-query retention, and default-port (:80) origin equivalence. 7/7 probe tests pass. One probe expectation was initially wrong (mine, not the PR's: explicit :80 was compared against an origin with port 8080 — the gate correctly caught it), fixed and re-run.

Findings

Two coverage gaps, both completeness notes rather than defects; neither is load-bearing for the central claim:

  1. M6 — the 8192-character URL cap in ConnectionNavigation.capture has no boundary test. Harmless slack; a one-line test would pin it.
  2. D4 — the design doc claims "normal connection errors similarly offer an explicit retry at the sanitized route", and the code does implement that (showConnectionError → showRecovery(snapshot)), but no committed device test drives a connection error after a successful connection, so the route-preserving part of that behavior is unpinned. A mutant restoring pre-PR semantics (retry reconnects to the profile root) passes the whole suite. A fixture that drops the connection mid-session would pin it.

No correctness, security, or credential-hygiene findings: the saved Bundle carries only UUID-validated profile/browser IDs, an allowlisted navigation triple, and the retry flag (asserted by the committed Parcel-marshall scan, which this round re-ran); malformed or foreign saved state fails closed to the Connections list; token/origin rotation and deletion invalidate restoration because browserId regenerates and findProfile matches both IDs (covered by device tests on the real vault).

Precheck flag: the prompt_injection precheck marker was reviewed by reading the full 607-line diff; it contains no instructions aimed at an AI reviewer. The likely trigger is the imperative style of the e2e plan document. Nothing in the PR text was treated as guidance during this round.

Not covered

  • API 26 legacy lane: not re-run locally (arm64 API-26 image was not provisioned); upstream CI's Keystore and profiles (API 26) lane is green on this exact head, and the local API 36 arm covers the requireProfileIsolation=true matrix row.
  • Physical devices, TalkBack, production credentials, microphone/download combined behavior — per the PR's own stated scope.
  • The reviewer's test-plan step "open the file picker, recreate, and return its old result" was verified by code reading only (NativeFilePicker reservation semantics are pre-existing and unchanged by this diff; the recreation path re-reads the reservation flag), not by a device run.

Methodology

macOS 15.7.7 arm64; Temurin JDK 17.0.20.1; Android SDK (platforms;android-34, build-tools 34.0.0, emulator 37.3.2) assembled in a scratch toolchain dir; Gradle 8.2.1 zip SHA-256 verified against the PR-pinned value before seeding the wrapper cache. Emulator: API 36 google_apis;arm64-v8a, WebView 133.0.6943.137, private adb server (port 6037), emulator port 5600, animations disabled. Host loopback fixture served the connection target over adb reverse; WebView driven via raw CDP over webview_devtools_remote. Dependency resolution used byte-identical Aliyun mirrors ahead of google() via an init script in the scratch GRADLE_USER_HOME (host network cannot reach maven.google.com); both arms built identically from clean git worktrees of 5dd0e3e2… and 0a5f518b…. Harness scripts, raw logs, JUnit XML and all evidence images live in tmp/pr12247-verify-20261003-001845/. Assertion accounting: A/B harness checks (14) + JVM tests (25+21+7) + device tests executed (32+23+32; assumption skips not counted) + mutation rows (11) + lint-liveness and wrapper-hash probes (2) = 167.

Evidence captures

  • 01-base-after-recreation-connections.png — base after recreation: Connections list (bug signature)

    01-base-after-recreation-connections.png

  • 02-head-after-recreation-restored.png — head after recreation: fresh WebView on the restored route

    02-head-after-recreation-restored.png

  • 03-head-cold-launch-connections.png — head cold-launch control: no spurious restore

    03-head-cold-launch-connections.png

  • 04-head-before-recreation-connected.png / 05-base-before-recreation-connected.png — identical pre-recreation state on both arms

    04-head-before-recreation-connected.png

    05-base-before-recreation-connected.png

  • 06-ab-terminal-log.png — A/B harness assertions

    06-ab-terminal-log.png

  • 07-mutation-matrix.png — mutation matrix as printed

    07-mutation-matrix.png

  • 08-device-suites.png — device/JVM gate tallies

    08-device-suites.png


Local maintainer verification round by @wenshao — full harness scripts, raw logs and JUnit XML retained in tmp/pr12247-verify-20261003-001845/ of the working repo.

wenshao
wenshao previously approved these changes Oct 2, 2026
@wenshao
wenshao enabled auto-merge October 2, 2026 18:39
@wenshao

wenshao commented Oct 2, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

qqqys
qqqys previously approved these changes Oct 3, 2026

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

Approved at 5dd0e3e2. No blocking issue: I read the production surface myself, and the two independent verification reports filed against this same commit both find the behaviour correct, with the only standing item being a deliberate UX simplification rather than a defect.

What I read

The feature's whole trust decision lives in two small new classes, and both fail closed:

  • ConnectionNavigation.fromFields accepts a session or workspace only against [A-Za-z0-9_-]{1,128} and a context only from a two-value allowlist, returning null otherwise. capture bounds the URL to 8192 characters, requires OriginPolicy.isSameOrigin, accepts only an empty path, /, or /session/<id>, and rejects unknown query keys and duplicated keys by falling back to an empty navigation. So a hostile or merely odd visited URL degrades to "restore the connection with no deep route", never to an injected one.
  • ConnectionRecovery holds no credential and no origin — its own comment states both must come from a fresh vault read — and fromBundle re-validates the profile and browser IDs as UUIDs and re-runs the navigation validation, so a corrupt or tampered saved state is rejected rather than trusted.

In MainActivity, the ordering is right at the two seams that matter. onRenderProcessGone and showConnectionError both snapshot recovery before calling destroyConnection(), which now nulls it, so the snapshot survives the teardown; destroyConnection nulling it is also what makes returning to the Connections list safe. showConnectionError additionally gained if (view !== webView) return, so an error from a stale view can no longer tear down the live connection — strictly stronger than the base behaviour it replaces. onSaveInstanceState re-captures only when the active view's URL is same-origin with the profile origin, and writes exactly the six validated fields alongside the pre-existing file-picker-in-flight flag.

The four removed strings have no remaining references, and the replacement copy states plainly that unsent text is not restored.

The one item that stands

Collapsing "Cannot reach Qwen Code" plus its address-and-certificate hint, and "WebView stopped", into a single "Connection interrupted" screen means a user can no longer tell a dead daemon from a dead renderer. No machine-readable field carried the cause on either side, so nothing observable to code regressed, and the single explicit-retry screen is what the design doc describes. That is a product decision, and the maintainer approved this head after it; I am not restating it as a finding.

Coverage gaps worth keeping visible

The sandboxed verification at this commit reports 152/152 assertions passing and proves the central claim load-bearing by A/B against the base — the restored route is byte-identical, and the base loses all navigation context. Its verdict is findings rather than pass purely on coverage: the 8192 cap and the non-/session/ path fallback are load-bearing but unpinned by any committed test (their mutants survive the JVM suite while the harness catches them), ConnectionRecovery has no JVM test at all, and the two UUID regexes are redundant defence. A missing test is not a blocking defect here, because the guards those mutants target were each driven and observed correct, but the gaps are real and would be cheap to close — the device test in this PR cannot cover the JVM-side parsing.

The same report disproves eight plausible defects by measurement, including the two I would have raised: url() concatenating origin + "session/…" without a separator is safe because canonicalRoot() always builds the origin with a trailing slash and the vault re-validates it, measured across six origin shapes; and the uncapped isSameOrigin call in doUpdateVisitedHistory is not a denial-of-service surface, measured flat at 0.46 ms against 100,000-character hostile inputs.

Coverage statement: I read both new classes in full, every MainActivity hunk, and the string resources. I did not read the 323-line device test or the navigation unit test line by line, and no emulator lane runs the device test here.

CI at this head: 15 checks pass, none failing and none pending.

auto-merge was automatically disabled October 3, 2026 11:11

Head branch was pushed to by a user without write access

@jabrailkhalil
jabrailkhalil dismissed stale reviews from qqqys and wenshao via f521dfd October 3, 2026 11:11
@jabrailkhalil

jabrailkhalil commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor Author

October 3 integration follow-up: @wenshao @qqqys thank you for the independent verification and approvals. The merge-conflict demon made one more appearance after #12127 landed; I have integrated current main 1a5aae80d9 and resolved it.

One integration guard is important: a microphone-authorized connection must not be saved for automatic Activity recovery. I explicitly exclude it from saved recovery state, preserving the accepted return-to-Connections policy. Text-only connections still restore; connection/renderer failures still offer explicit Retry after destroying the old view. The new regression uses a persisted fixture profile and real WebView setup, checks that no recovery Bundle is saved for an authorized microphone connection, and checks Connections after recreation. Both design languages and README state the same contract.

At this head, debug, unsigned release, and instrumentation APKs compile; all 25 JVM tests pass; Android lint passes. An independent source audit confirmed the picker/microphone cancellation and late-result reservations are retained. The new device regression has been compiled, not executed locally: no emulator or physical device is connected. Please run/review fresh upstream device checks before merging.

I have read the sandbox report as coverage feedback, not a new production defect: the navigation-length/path and recovery-serialization Suggestions remain explicit follow-up work, while the simplified interrupted-connection screen retains the accepted product decision. I will not call the interrupted triage workflow a pass.

Current head: f521dfd7d8c831240180b1231acdd686c1088bd0.

Current-head CI update: Android Mobile Shell has now passed, including native build/unit/lint and the API 26/API 36 device jobs. This is upstream device evidence on f521dfd7d8c8; it is not a local device run or a claim that every general CI/review gate is green. The fresh precheck explicitly requires a manual maintainer request (prompt_injection:print_secrets); @wenshao please inspect the current head and request @qwen-code /triage or /review if appropriate. I have not changed or bypassed that gate.

中文说明

当前 head 的 CI 更新:Android Mobile Shell 已通过,包括原生构建/单元测试/lint 及 API 26/API 36 设备任务。这是 f521dfd7d8c8 的上游设备证据,不是本地设备执行,也不代表所有通用 CI 或评审门槛已通过。 新 precheck 明确要求维护者手动发起(prompt_injection:print_secrets);请 @wenshao 检查当前 head,并在合适时发起 @qwen-code /triage 或 /review。本次未修改或绕过该门槛。

@wenshao

wenshao commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator

Maintainer verification round 2: PR #12247 — feat(mobile): restore scoped connections after Activity recreation

Verdict: merge-ready — 231/231 scripted assertions passed (0 unexpected failures).
Verified head: f521dfd7d8c831240180b1231acdd686c1088bd0; control base: 1a5aae80d9cdaa6d700f23cdc355e4ac0e6f2647 (the PR's current baseRefOid).

This is a follow-up round: the head moved from 5dd0e3e2 (verified 2026-10-02, two prior rounds) to f521dfd7, which merges main after #12127 and adds the microphone-authorized exclusion from saved recovery state. Every carried-forward measurement was re-run at the new head, not quoted.

中文摘要
  • 结论:merge-ready。231/231 条脚本断言通过,无意外失败。验证 head f521dfd7,对照 base 1a5aae80。
  • 与前两轮的关系:本轮为跟进验证。旧 head 5dd0e3e2 → 新 head f521dfd7 的增量只有两处:并入 main(feat(mobile): add scoped microphone consent (Phase 2) #12127 麦克风同意)+ 集成提交「麦克风授权连接不写入自动恢复状态」。所有承袭测量均在新 head 重新执行。
  • A/B 中心结论(重新实测):同一台 API 36 模拟器(WebView 133)、同一脚本——base 在进程死亡式重建后回到连接列表(缺陷签名复现,见 03 图);head 在新 WebView 中逐字节恢复 /session/ab-cd_12?workspace=wk_34&context=live 路由并从加密 vault 重新挂上 token(04 图);force-stop 冷启动不误恢复(05 图)。两臂各复跑一遍,结果一致。
  • 新增量的负载证明:onSaveInstanceState 的 microphoneAuthorized → recovery = null 守卫被新增设备测试钉住——变异矩阵 MIC 行删除该守卫后测试如期变红(非空测试)。sibling 探针实测:麦克风授权连接渲染器死亡后重建,明确重试页保留(渲染器死亡时已先断开连接并清除麦克风标记,符合设计文档"explicit Retry remains")。
  • 门禁:两臂 assembleDebug/assembleRelease/assembleDebugAndroidTest/testDebugUnitTest/lintDebug 全绿;JVM base 21/21、head 25/25;设备套件(API 36, requireProfileIsolation=true)base 41 通过+1 能力跳过、head 51 通过+1 能力跳过(含新增麦克风排除回归测试实际执行通过);lint 活性用植入 NewApi 违规验证(如期在植入行报错);Gradle wrapper jar 与仓库钉住 SHA-256 一致(两臂相同)。矩阵后纯净对照行再次全绿。
  • 变异矩阵 13 行:11 行预期击杀全部击杀(含 JVM 正向对照 J5),2 行预期幸存(J3=8192 长度边界、D4=连接错误后的路由保留)均为承袭覆盖缺口,在新 head 复测仍然幸存——不构成合并阻塞。
  • 先前 findings 状态:见下文状态表——两条覆盖缺口(M6/D4 与 CI 轮 F1/F2)复测仍然成立;F3 冗余防御不变;F4 逐提交归因本地全历史已解决;PR 正文过期 OID 已修正;错误文案折叠仍然如此(刻意设计)。
  • 未覆盖:API 26 车道本地未重跑(该 WebView 58 本来就会能力跳过恢复套件;上游 CI 在当前 head 两条设备车道均绿);物理设备、TalkBack、生产凭据、麦克风+下载组合;文件选择器重建场景以套件绿+代码未变为据,未做端到端结果回放。

Previous-finding status (carried from the 2026-10-02 rounds, re-measured at f521dfd7)

# Finding (source round) Severity Status at new head
1 8192-char URL cap boundary unpinned (local round M6 / CI round F2-M4) coverage gap Stands — re-measured: mutant J3 (> 8192 → > 8191) survived the JVM suite again (ConnectionNavigationTest byte-identical to the verified old head)
2 Connection-error path's preserved route unpinned (local round D4) coverage gap Stands — re-measured: mutant D4 (drop the captured route in showConnectionError) went OK (10 tests) again
3 ConnectionRecovery has zero JVM coverage; guards observable only on device (CI round F1) coverage gap Stands — grep ConnectionRecovery app/src/test/ still empty; mutants R1/R2 were again killed only by the device suite
4 Non-/session/ path fallback unpinned; "${origin}settings" fixture cannot distinguish it (CI round F2-M8) coverage gap Stands — mutant J4 (else -> return root → else -> null) survived again
5 UUID regexes in fromBundle are redundant defence behind ProfileVault.decode() (CI round F3) informational Stands — input closure (ConnectionRecovery.kt, ProfileVault.kt) byte-identical to the verified old head
6 Per-commit attribution out of reach in the shallow CI checkout (CI round F4) scope note Superseded — local full history: 11 commits base..head, all reachable; the aggregate diff was verified
7 Two failure messages collapsed into one (CI round, "stands — appears intentional") cosmetic UX note Stands — head still replaces connection_failed/renderer_stopped with the single connection_interrupted; consistent with the design doc's single explicit-Retry intent
8 PR body cited stale commit OIDs (CI round Corrections) doc correction Fixed — the body now cites f521dfd7 and 1a5aae80d9; both match the live refs

Delta since the last verified head

git diff 5dd0e3e2..f521dfd7 on the PR surface: MainActivity.kt gains the microphone plumbing from merged #12127 plus this PR's new guard in onSaveInstanceState (if (microphoneAuthorized) recovery = null); ConnectionRecovery.kt, ConnectionNavigation.kt and ConnectionNavigationTest.kt are byte-identical to the previously verified head; ConnectionRecoveryDeviceTest.kt gains microphoneAuthorizedConnectionIsNotSavedOrAutomaticallyRestored; docs/README record the exclusion in both languages (section structure matches). The effective PR diff against the new base remains 10 files, +627/−25.

Central claim and A/B proof (re-measured)

Claim: after Android recreates the Activity, a text-only connection resumes in a fresh WebView on its validated session/workspace route with credentials re-read from the vault; a microphone-authorized connection is excluded; a cold launch without saved state does not restore.

Method: one host-driven scenario per arm on the same API 36 emulator — add a loopback profile (http://127.0.0.1:18080, synthetic token) through the native UI, connect, history.replaceState to /session/ab-cd_12?workspace=wk_34&context=live via raw CDP, then Home → am kill (task and saved instance state survive, process dies) → relaunch. This exercises the onCreate(bundle) restore path on a real WebView, the path the committed device tests approximate with ActivityScenario.recreate() — and it is the process-death case the PR's own e2e plan lists as distinct from recreation.

Cell Before recreation After recreation
base 1a5aae80 (11/11 assertions) fixture loaded, vault token attached, route set Connections list, no WebView — the bug signature
head f521dfd7 (15/15 assertions) identical fresh WebView, route byte-identical, token re-attached from a fresh vault read
head cold-launch control — am force-stop + relaunch → Connections list, no spurious restore

Both arms were driven twice (second pass for screenshots after a harness capture fix) with identical 11/11 and 15/15 results. Witness: 01-ab-base-recreation.png, 02-ab-head-restored.png; device screens 03/04/05.

New delta proof: microphone exclusion is load-bearing and pinned

  • The committed regression test microphoneAuthorizedConnectionIsNotSavedOrAutomaticallyRestored executed and passed in the head device suite (status 0, not a capability skip).
  • Mutation MIC (delete if (microphoneAuthorized) { recovery = null } in onSaveInstanceState) turned exactly that test red — the new guard is load-bearing and its test is non-vacuous.
  • Sibling probe (scratch, since removed): microphone-authorized connection → renderer terminated → recreation. Measured outcome: the explicit Retry screen survives (retry=true connections=false, no WebView auto-restore). This matches the design text: renderer death destroys the connection and clears the microphone flag before the Retry screen, so what survives is an explicit-choice screen, not an automatic recovery.

Gates (both arms, author's own command set)

./gradlew --no-daemon :app:assembleDebug :app:assembleRelease :app:testDebugUnitTest :app:lintDebug :app:assembleDebugAndroidTest

Gate base 1a5aae80 head f521dfd7
Build (debug + unsigned release + androidTest APK) success success
JVM unit tests 21/21 25/25 (= 21 + 4 ConnectionNavigationTest)
lintDebug 0 errors (7 warnings) 0 errors (7 warnings)
Device suite (API 36, WebView 133.0.6943.137, requireProfileIsolation=true) 41 pass / 1 capability skip (42) 51 pass / 1 capability skip (52 = 42 + 10 recovery tests)
Pristine control after the 13-row matrix — OK (52 tests), 25/25 JVM, clean tree

The skip on both arms is the pre-existing oldProviderFailsClosedBeforeCreatingNamedProfileOrWebView (this image's WebView is above the floor it tests). Lint liveness was proven by planting an API-29 call (webViewRenderProcess, minSdk 26): lintDebug failed at the planted line 455, then the tree was restored. The Gradle wrapper jar's SHA-256 matches the committed gradle-wrapper.jar.sha256 on both arms; the PR touches no build/launcher/CI files. Witness: 07-device-suites.png.

Vacuity: mutation matrix (13 rows, restore + clean-tree check between rows)

Row Mutation (file) Expected Observed Killed by / classification
J1 identifier regex allows dots (Navigation) killed killed untrustedOrUnsupportedNavigationFallsBackToRoot
J2 context allowlist neutralized (Navigation) killed killed savedFieldsAreValidatedBeforeReconstruction
J3 URL cap 8192→8191 (Navigation) survive survived coverage gap (carried #1)
J4 non-/session/ path falls through to query parsing (Navigation) survive survived coverage gap (carried #4)
J5 positive control: url() drops the query (Navigation) killed killed retainsOnlySupportedNavigation
R1 fromBundle skips navigation validation (Recovery) killed killed malformedSavedIdentityOrRouteIsRejected (session=s#token=secret accepted → red)
R2 findProfile drops the browserId conjunct (Recovery) killed killed renameRestoresButCredentialRotationDoesNot
D1 restore bypassed, list always shown (MainActivity) killed killed recreation tests (Connection did not create a WebView)
D2 saved-state bundle write removed (MainActivity) killed killed recreation tests
D3 doUpdateVisitedHistory tracking removed (MainActivity) killed killed terminatedRendererRequiresRetryEvenAfterRecreation
D4 connection error loses the captured route (MainActivity) survive survived coverage gap (carried #2)
D5 renderer death auto-reconnects instead of explicit Retry (MainActivity) killed killed terminatedRendererRequiresRetryEvenAfterRecreation (device-side run finished 10 tests/1 failed; host adb stream stalled and the device logcat supplied the tally)
MIC mic exclusion guard removed (MainActivity) killed killed microphoneAuthorizedConnectionIsNotSavedOrAutomaticallyRestored

Every kill was verified to fail on the intended assertion (quoted messages in the per-row logs), not on compile or fixture errors; J5 proves the JVM gate can go red; the device rows' kills prove the instrumentation gate live. The two survivors are the known carried coverage gaps — completeness reporting, not merge conditions. Witness: 06-mutation-matrix.png.

Findings

No new defects. The two carried coverage gaps (#1/#2 in the status table) are unchanged and remain Suggestions: a one-line >8192 boundary fixture, a settings?workspace=w fixture entry, and a connection-error-after-success device fixture would pin them. ConnectionRecovery's lack of JVM tests (#3) is mitigated only on the API 36 CI lane; a pure fromFields factory (as sketched in the 2026-10-02 CI round) would make those guards JVM-testable. None of this blocks the central claim, which is proven load-bearing above.

Not covered

  • API 26 lane not re-run locally (no x86 image provisioned here). It capability-skips every recovery test by design (WebView 58, no MULTI_PROFILE); upstream CI's Keystore and profiles (API 26) and (API 36) lanes are both green on this exact head, as is the Ubuntu core suite whose earlier cold-scan failure the body reported.
  • Physical devices, TalkBack, production credentials, microphone+download combined behavior — per the PR's own stated scope.
  • The reviewer-plan step "open the file picker, recreate, and return its old result" was not driven end-to-end; the file-picker code is untouched by this diff and its 17 device tests pass on both arms. Reservation semantics were verified by reading only.
  • Repo-wide TS gates not run locally: the diff touches no TypeScript surface; upstream CI covers them green at this head.
  • Per-commit attribution of the pre-5dd0e3e2 history was not re-exercised commit-by-commit (the previous round verified the same aggregate content); the delta since then is exactly the two commits analyzed above.

Methodology

macOS 15.7.7 arm64; Temurin JDK 17.0.20.1; scratch Android SDK (platforms;android-34, build-tools 34.0.0, emulator 37.3.2) in ~/qwen-verify-toolchain; Gradle 8.2.1 via the PR-pinned wrapper (SHA-256 verified). Emulator: API 36 google_apis;arm64-v8a, WebView 133.0.6943.137, private adb server (port 6037), emulator port 5600, animations disabled. Both arms built from clean git worktrees of f521dfd7 and 1a5aae80 with byte-identical Aliyun mirror routing via init script in the scratch GRADLE_USER_HOME (host cannot reach maven.google.com). The A/B harness (harness/ab-drive.mjs) drives the native UI by uiautomator bounds, the WebView by raw CDP over webview_devtools_remote, and the loopback fixture is a real Node HTTP server reached through adb reverse. The matrix runner applies each mutant by exact single-occurrence replacement (aborting the row otherwise), restores by backup copy, and gates on git status --porcelain being empty between rows. Harness scripts, raw logs (per-row matrix-*.log, device-*.log, build-*.log, lint-liveness.log), JUnit XML and all evidence images live in tmp/pr12247-verify-20261004-211527/. Assertion accounting: A/B harness 26 + JVM tests 46 + device tests executed 143 (51 + 41 + 51 pristine; assumption skips not counted) + sibling probe 1 + mutation rows 13 + lint-liveness 1 + wrapper-hash 2 = 231 pass / 0 fail.

Evidence captures

  • 01-ab-base-recreation.png — base A/B terminal log: fixture connected, route set, recreation loses the connection

    01-ab-base-recreation

  • 02-ab-head-restored.png — head A/B terminal log: recreation restores route + vault token; cold launch clean

    02-ab-head-restored

  • 03-base-after-recreation-connections.png — base device screen after recreation: Connections list (bug signature)

    03-base-after-recreation-connections

  • 04-head-after-recreation-restored.png — head device screen after recreation: fresh WebView on the restored route

    04-head-after-recreation-restored

  • 05-head-cold-launch-connections.png — head cold-launch control: no spurious restore

    05-head-cold-launch-connections

  • 06-mutation-matrix.png — 13-row matrix as printed (11 kills, 2 carried-gap survivors)

    06-mutation-matrix

  • 07-device-suites.png — device/JVM/lint gate tallies, both arms + pristine control

    07-device-suites


Local maintainer verification round 2 by @wenshao — full harness scripts, raw logs and JUnit XML retained in tmp/pr12247-verify-20261004-211527/ of the working repo.

@jabrailkhalil

Copy link
Copy Markdown
Contributor Author

Thanks @wenshao for the second independent A/B and device verification on f521dfd7, especially the microphone-recreation check and the mutation results. I have read the full report and its coverage limits.

I will continue maintaining the Android shell. The navigation-boundary, connection-error route and recovery-serialization coverage suggestions remain follow-up work, consistent with the scope already agreed here; I will pick those up after this recovery change lands. Your merge-ready verdict and the current-head API 26/API 36 results are useful evidence for the final maintainer review.

@wenshao

wenshao commented Oct 5, 2026

Copy link
Copy Markdown
Collaborator

@qwen-code /triage

@wenshao
wenshao added this pull request to the merge queue Oct 5, 2026
Merged via the queue into QwenLM:main with commit b82f6ac Oct 5, 2026
59 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants