# gotchas.md — 关键实现细节与坑(全量) > grep 用法:`grep -n "<坑号>\." docs/AGENTS/gotchas.md` 或按关键词(如 `borrowSession`、`MANAGE_EXTERNAL_STORAGE`、`store-rehome`)定位。 > 维护约定:新坑追加到文末并递增编号;修复后保留条目(教训与历史归档)。 ## 6. 关键实现细节与坑(每次踩坑必须登记) 1. **realpath 前缀混用(B7 运行时表现,已修)**:Android 上 `/data/user/0` 可能是 `/data/data` 的软链——只把「文件侧」realpath 后再与未 realpath 的 ws 比较必拒。修复:`safeResolveInside` **两侧都 realpath**(ws 侧失败按原样参与),symlink 目标按 `dirname(rel)` 解析(`../../LICENSES` 从 `doc//` 出发 = `share/LICENSES`)。**新路径校验代码一律双侧规范化。** 2. **会话 header meta 白名单**:`Session.create(meta)` 的 `origin` 只允许 `"subagent"`——自定义值报 `session header origin must be "subagent"`;只写白名单键(如 `cwd`)。 3. **surface 事件必须带 surfaceOp**:`user/message` 等 surface-eligible 事件 append 需第 3 参 `{surfaceOp:'append'}`,否则 `requires a surfaceOp marker`。 4. **pnpm 是市场安装的硬依赖(已固化进快照构建)**:`dsh plugin add` spawns `pnpm`(apps/cli plugin.ts);快照缺 pnpm → `pnpm not found on PATH`;且**陈旧 pnpm 状态记录**(base-dsh 里的 `.modules.yaml`/`.pnpm-workspace-state`/`pnpm-lock`)指向旧 store → `ERR_PNPM_UNEXPECTED_STORE`——build-snapshot-013.mjs 已装配 pnpm 10.12.1 standalone(npm tgz + `usr/bin/pnpm` shim)并清理这三种记录。**市场目录里的部分插件(如 humanizer-ru)不在 npm,安装 404 属上游目录数据,不是本链路缺陷。** 5. **快照 node 需要 OPENSSL_CONF**:运行快照内 node 时务必与 UndoGate 同样注入(否则 OpenSSL config error 静默吞 CLI 输出)。 6. **Windows 侧读 WSL 9p 文件 = EACCES**:stage 文件不可直 stat/read;校验类代码走 `--tar`(wsl tar -tvf 带大小)视图;wsl.exe 输出前有 localhost 代理噪音行,解析时过滤。 7. **npm registry 元数据可能缺 dist.sha512**:pnpm tgz 完整性校验「在场则严格,缺席降级警告」。 8. **run-as 引号地狱 / adb 二进制传输**:PowerShell 双引号内 `$var` 本地展开;二进制经 `adb exec-out`/push 传输。 9. **template 字符串反斜杠**(页面注入):`\n` 双写。 10. **签名一致性**:debug.keystore 固定,否则覆盖安装失败。 11. **CDP 断言注意**:input placeholder 不在 innerText(查 `[placeholder]`);「live 会话禁止 API prompt」;`session.list` 的 stats 字段(turns/llmMs)判断代理是否真跑。 12. **引擎级 OPENSSL_CONF 曾有缺口(2026-08-24 真机实锤,已修)**:快照 node 编译期硬编码 OpenSSL 配置路径 `/data/data/com.termux/files/usr/etc/tls/openssl.cnf`(app 域不可读)——`shellEnv()` 未注入 OPENSSL_CONF 时,**任何 node/npm 子进程(agent 工具调用)启动即 OpenSSL configuration error 退出**;引擎本体侥幸存活(不触发该初始化的路径)。修复 = `shellEnv()` 加 `OPENSSL_CONF=/etc/tls/openssl.cnf`(与 UndoGate/AdbState 统一;坑 #5 是本坑在 CLI 面的显式版)。 13. **apt/dpkg 编译期路径(issue #80,2026-08-24 重写)**:apt/apt-get/dpkg 二进制内置 `/data/data/com.termux/files/usr` 编译期路径。`-o Dir::Etc=...` 参数覆盖不了 apt.conf.d 早期扫描(仍报 Permission denied);**有效方案 = APT_CONFIG 主文件**(build-snapshot-013.mjs 7d 段生成 `usr/etc/apt/apt.conf`,wrapper 统一 `export APT_CONFIG`)——注意 `K='...$B...'` 单引号不展开曾令 wrapper 失效。**dpkg 的 SYSCONFDIR(dpkg.cfg.d 配置目录)无 env 可覆盖**(strings 证实无 DPKG_CONFIG_DIR 变量;`--admindir/--instdir` 不覆盖)→ apt 在线安装 dpkg 阶段受限;`scripts/check-prefix-residue.sh` 设备端自检验证。 14. **配对伪成功防御(2026-08-24 真机实锤)**:`nativeBridge()?.setAdbPair?.()` 可选链在桥缺失/方法缺失时返回 undefined → `ok === false` 恒 false → 前端误报「配对完成」——**显式检查 `typeof b.setAdbPair === 'function'` 且无函数即 throw**。设置页两端口输入框是必经项,用户嫌手动抄录——0.13.0 起端口发现用 **NSD/mDNS**(AdbState.discoverPorts:系统属性直读优先 → `_adb-tls-pairing._tcp`/`_adb-tls-connect._tcp` NSD 发现(5s 超时)→ 手动输入硬回退;盲扫 37000-45999 已剔除,Q17)。 15. **临时工作区面板不可见(issue #60,2026-08-24 已修)**:workspace registry 只从**既有会话 cwd** bootstrap——无会话时「临时工作区」不出现在工作区面板。修复:dsh-android-file-open apply 时 `workspaceRegistry.create(tmpWorkspace(), '临时工作区')`(幂等复用)。TTL 清理(7 天)壳侧 FileIncoming.sweepExpired(启动 + 每次入队前)。 16. **老内核 ES2022 polyfill(issue apk#81/#79)**:华为/荣耀/小米定制 WebView(Chromium<92)缺 `Object.hasOwn`/`Array.at`/`String.at` → 前端加载插件报 "Failed to load plugins"。polyfill 注入点 = `assets/patched/web-frontend-index.html` `` 首个 script 之前(引擎 applyAssetPatch 用它替换内核 index.html);**升级 dsh 后其模块脚本哈希(index-ClqxG24t.js)须同步更新**。 17. **错位目录(issue #80 P5)**:relocate-snapshot 曾把包内绝对路径 `/data/data/com.termux` 当相对路径搬进 usr 树(`usr/data/data/...`)——纯冗余;构建链 7e 无条件删除 `usr/data`。 18. **debug APK 默认 x86_64 快照(2026-08-24 本日重大事故)**:`app/build.gradle.kts` mergeDebugAssets 注释写死「从 GitHub Releases 下载 snapshot-x86_64.tar.xz 放 assets」→ `app-debug.apk` 内快照是 x86_64;**装到 arm64 真机覆盖终版后引擎崩**:`error: "/data/data/.../usr/bin/node" is for EM_X86_64 (62) instead of EM_AARCH64 (183)`。核对方法:解快照 tar 读 `usr/bin/node` 的 ELF e_machine(62=x86_64,183=arm64),或 `aapt dump badging ` 看 native-code。修复/预防:构建/安装前核对设备 ABI 与快照 ABI;换 arm64 快照须**同时替换** `assets/snapshot.tar.xz` + `snapshot.sha256`(指纹变→refreshSnapshot 全量重解压 2-4 分钟,勿中断)。 19. **cordis.patch.yml 装配缺陷**:基座 cordis.patch.yml 只含 shell-termux/host-web-compat/ui-responsive——0.13.0 新增的 android-bridge/android-manage/android-linux-env/android-file-open/undo-savepoint/marketplace **从不进入快照装配** → 引擎不加载这些插件(`/api/android/file-incoming` 404、ADB 设置项缺失、通知事件桥无宿主)。修复:build-snapshot-013.mjs 7b2 用 `scripts/profile-web.cordis.patch.yml` **权威覆盖**快照内同名文件(缺失即 `process.exit` 拒发)。**注意**:真机热改 cordis.patch.yml 后必须**冷启动 app**(`am force-stop` + start)才重装配——watchdog 热重启引擎不重读 profile(只重跑 node)。 20. **EngineService 挂载缺失**:startEngineService 只在 startEngineFlow 首次轮询成功时调用——**引擎先跑、app 后启动(热启动/恢复)时服务从未启动 → watchdog 缺失 → 通知消费(task-done 标记)/自动回退/唤醒锁全链路失效**。修复:MainActivity `onResume` 幂等 `startEngineService()`。 21. **通知链路三缺(2026-08-24 用户实测「任务完成没通知」根因)**:① 引擎事件桥缺失(前端 showNotification 桥无调用方,F0.3 部分实现=以前未做)——补:dsh-android-bridge 监听 `ctx.on('session/event')` 捕获 `assistant/message` → 写 `files/home/.dsh/.task-done.ndjson` 标记;② WatchdogV2.consumeTaskDoneMarkers 消费标记 → `NotifyCenter.notify`(deepProbe 成功路径顺带;JSON 解析失败会丢通知——务必确保标记是 `JSON.stringify` 合法 JSON);③ EngineService 必须挂载(见坑 20)。验证:`files/notify-debug.log` 落盘每步(诊断时开);通知 id=`("dsh-"+category).hashCode() and 0x7fffffff` 稳定正数。 22. **免 hooks 环境 ELF 不可执行**:`adb shell run-as ... /usr/bin/ | head` 会报 `not executable: 64-bit ELF file` / `CANNOT LINK ... library libandroid-support.so not found`——因为 run-as 裸环境无 termux-exec LD_PRELOAD 钩子与 LD_LIBRARY_PATH。**验证快照内二进制必须带全套引擎 env**(`LD_PRELOAD=libtermux-exec-ld-preload.so` + `TERMUX_EXEC__*` + `LD_LIBRARY_PATH` + `OPENSSL_CONF`)。这是「run-as 测出假错误」的常见来源(如 node/npm/adb)。 23. **通知 debug 落盘**:WatchdogV2.consumeTaskDoneMarkers 写了 `files/notify-debug.log`(诊断用,**保留**——是排查通知链路的关键工具)。 24. **adb client 冷启动 server 必败(2026-08-27 真机实锤,已修)**:termux-exec/Linker64 重路由环境下,`adb pair` 首次调用自动起 server(fork-server)时握手坏——client 打印 "* daemon started successfully" 后读不到应答,报 `error: protocol fault (couldn't read status message)`,**配对必失败(用户侧观感"光速报错")**。对照实验证明:client 直连**已存在**的 server(`adb server nodaemon` 常驻)时 pair/connect 全部正常(同码同端口复刻 2/2 成功)。修复 = AdbState 壳内自管常驻 server(runAdb 前 `ensureAdbServer`:spawn `server nodaemon` + 5037 探测 + 日志落 `files/home/adb-server.log`)+ `retryRunAdb`(protocol fault 时毁 server 重建重试一次);pair 失败首行错误文本入审计(不含码)。另:discoverPorts 同步 NSD 等待 5s 会卡配对页 UI(bridge 同步调用链)——超时压到 2s + 启动后台预取 + 15s TTL 缓存(`cachedPorts`),bridge 缓存优先。**补锤**:app 域直接 exec app-data ELF 恒 EACCES(error=13,server/client 双双中招,审计 error 字段实锤),spawn 一律走 `spawnAdb`(捕获 Permission denied 降级 `/system/bin/linker64` 加载——EngineManager.startWithArgs 同机制)。 25. **配对码窗口被自家冷启动链耗光(2026-08-27 复盘实锤,三探针 logcat/audit/adb-server 时间线定案,已修 F1+F2+F3+F4)**:系统「使用配对码配对」弹窗的端口仅在弹窗存活期监听;用户点「配对」后壳侧才冷启动(linker64 加载 ~3s + 回收配对后密钥删除触发全新 RSA keygen + 首轮 client 握手竞态必吃 protocol fault 再自愈重建),合计 7 秒以上,拨号时窗口已关 → 恒 `Connection refused`(pairing_client.cpp),用户在系统弹窗上点什么都不救得回来。修复四件套 = **F1 常驻预热**(`AdbState.prewarm`:getAdbState 轮询钩子/onResume 后台线程拉起,60s 节流;**密钥生成移出关键路径**——回收配对会删 `$HOME/.android/adbkey*`,下次任何连接都会重新生成)+ **F2 真实就绪判定**(5037 bind ≠ 可服务:`ensureAdbServer` 放行条件收紧为一次真实 `devices` 客户端往返通过,`serverReady` 标志贯穿 spawn/复用孤儿/self-heal 三路径)+ **F3 结构化配对结果**(`setAdbPair` 返回 JSON `{ok, reason, message}` 替代 Boolean,壳侧 `classifyFailure` 归因 window-closed/protocol-fault/server-not-ready/handshake-timeout 等)+ **F4 前端分流**(AdbAuthSection 轮询不再抹操作报错——双通道 pollError/actionError 且后者留存 15s;失败不清空输入框;文案按 reason 分流,refused 明示「窗口已关闭请重开弹窗」而非误导性「核对 6 位码」)。 26. **vivo SELinux 拒读无线调试属性(2026-08-27 logcat 实锤)**:untrusted_app 读 `service.adb.tls.pairing_port` / `service.adb.tls.port` 触发 `avc denied { read } adbd_prop`——AdbState.discoverPorts 的「系统属性直读」优先路径在 vivo OriginOS 上恒失效(异常路径静默吞掉无感知),实际全靠 NSD/mDNS 兜底。勿据此属性在 vivo 上做正确性假设;后续若在此设备上看到直读成功属 ROM 变更,需回归。 27. **引擎侧页面误调 openNativePath(2026-08-27 记录在案)**:logcat 出现 `dsh-image: openNativePath: not exists: 自动扫描系统无线调试的配对/连接端口…`——引擎 UI 包把按钮 title 文案当路径传给了桥调用。壳侧安全拒绝 no-op 无实害;根因在上游引擎页面包(非本仓库管辖),升级引擎时留意。 28. **dsh-shell run() 返回 CollectedOutput 结构体(2026-08-27 活体插桩实锤,已修)**:`shellFace.run()` 契约返回 `{stdout:{text,truncated,spillPath?}, stderr:{…}, exitCode,…}` 而非字符串(引擎内置 bash 工具经 `streamText(output).text` 同款读取)。插件历史代码 `String(r.stdout)` 直取 → 恒 `"​[object Object]"`:**进程真实执行(审计恒 ok)、模型转录全毁**,并连带 device_info 满屏 `?` 占位(拿乱码 grep MODEL= 零匹配)与 lossless 拒收(可选字段 undefined 成员被引擎整值拒绝)。修复 = `collectText()` 解包 + `pickText()` 类型闸 render(非 string 一律 JSON 转写,杜绝 [object Object] 再入转录)+ 可选成员空串兜底。**伴生雷**:NSD 抓的连接端口随无线调试重启轮换(37575 失联实锤)→ `resolveLivePort()` 配置端口失效即回退 5555。**排障方法论沉淀**:①疑似「改码不改行为」先杀引擎进程再验——`force-stop` 后可能有孤儿 `node -`(linker64 链)幸存占 5037/3080 继续用旧模块(/proc 扫 cmdline 对照注入 mtime);②设备端插件注入用 base64 分块 `printf %s >> ` 通道(MSYS /tmp 不跨执行块存活,stdin 管道会截断);③插桩指纹(状态消息缀 ⟪标记⟫)一次往返即可判定加载版本。 29. **引擎会话档位为事件溯源、按会话隔离(2026-08-27 澄清,非缺陷)**:UI 档位选择器写入会话日志 `sandbox/mode` 事件(`effectiveSandboxMode` fold,最后一条生效),重启经重放恢复、两会话互不可见;`session.list` projections.permissions.currentValue 为真值。状态端点显示的 `writeMode=workspace-write` 仅部署默认(装配 yml shell-termux config),工具实际放行以会话档位为准(gateFor→sandboxPolicy.resolve)。勿把部署默认当死锁。 30. **ps1 双 ABI 循环后 assets 停留 x86_64(2026-08-29 实锤,坑 18 现代版)**:build-apk-013.ps1 循环内按 ABI 覆盖 `assets/snapshot.tar.xz`,循环结束留在 x86_64——此后直接 `gradlew assembleDebug` 的 debug 包即 x86 树,装 arm64 真机报 EM_X86_64(本轮已踩)。铁律:真机安装只用 ps1 对应 ABI 命名产物;存疑时 `od -A d -j 18 -N 2 -t u1` 读 node ELF 机器码(183=arm64/62=x86_64)。 31. **force-stop 杀不死 linker64 回退子进程(2026-08-29 vivo 实锤,0.14 修复)**:升级后旧引擎孤儿存活 → 双引擎抢 3080(探活打到旧引擎、新引擎 bind 失败循环;两代 engine.log 交错误导排障)。真机找引擎 `ps -A | grep linker64`(进程名非 node,pidof node 必空);处置:run-as kill 全部 linker64 → 看门狗 ~7s 自愈。`pkill -f bin.js` 在 vivo 疑似不生效(坑 28 排障方法论①的 /proc cmdline 扫描为可靠手段)。 32. **adb forward 静默失效(2026-08-29 实锤)**:APK 重装/USB 重枚举后宿主 forward 清空 → 宿主探活 000,但设备内正常(用户 WebView 秒起)——「引擎挂了」的判断必须先 `adb forward --list` 再重 forward,否则误诊。 33. **系统 HTTP 代理劫持壳侧本地探针(#118 根因1,2026-09-02 实锤)**:WiFi 配置系统代理时,`HttpURLConnection.openConnection()` 默认走 `ProxySelector` 下发的系统代理 → 把 `127.0.0.1:3080` 的本地请求发给代理网关(回不来本机)→ 探针恒 timeout;而 WebView(Chromium 对 loopback 豁免代理)与 curl(不读系统代理)直连正常 → 「网页能开、app 却判引擎没起来」的矛盾现场。修复 = **壳侧所有本地引擎端口调用一律 `openConnection(Proxy.NO_PROXY)`**(EngineProbe / file-incoming / session.export / session.cancel;UpdateManager 的远程下载**不走** NO_PROXY),且诊断包 probe 字段区分 timeout/refused。 34. **UndoGate.runCli 直接 exec app-data ELF 无 linker64 fallback(#118 根因2,2026-09-02 修)**:`runCli` 直接 `ProcessBuilder` exec `usr/bin/node`(对比 `EngineManager.startWithArgs` 有 `/system/bin/linker64` fallback)——Android 15+ 拒绝直接 exec app-data ELF → error=13 → auto-undo 从未真正执行。修复 = 与 startWithArgs 同款:捕获「Permission denied」降级 `linker64` 加载(`build` 复用 shellEnv/OPENSSL_CONF/redirectErrorStream)。 35. **requestLegacyExternalStorage 对 targetSdk≥30 应用无效(#120/MT 调研,2026-09-02 实锤)**:该 flag 仅对「targetSdk≤29 + 运行在 Android 10」生效;targetSdk≥30(含 34)应用即使运行在 Android 10 设备上 flag 也被忽略(SO 63365334 / cgeo #10386 / 小米适配指南多源实锤)→ Android 10 上 SAF 树授权不解锁 FUSE 原始路径、bash 走不了 ContentResolver → 非 root 下「读用户任意目录作工作区」不可达成。落地:SDK 26-28(无分区存储)运行时 READ/WRITE 权限放行;SDK 29 保留拒绝但显式 reason(`__dsh_pick_refused__:android-10`)而非伪装取消;SDK 30+ 维持 All Files Access。 36. **自包含内置子仓副本必须与协调仓同版(2026-09-02 实锤)**:本仓库是**云端自包含构建宿主**(`.github/workflows/build-apk.yml` 依赖 `$GITHUB_WORKSPACE`=本仓库,不签协调私库),仓库内自带整套子仓副本(`dsh-shell-termux`、`dsh-client-ui-responsive`、`dsh-host-web-compat`、`plugins/dsh-android-*`)。**协调仓改动这些子仓的源码或 bump 版本后,必须把产物同步进本仓库对应子目录(src + package.json + lib/),否则自包含链(云端构建 + `gradlew assembleDebug` 直打)注入的是旧副本**——设备上表现「悬浮球开关消失 / ADB 面板缺失 / #120 拒绝信号缺席 / 配置导入导出按钮消失」这类**功能性缺失但编译通过**的幽灵缺陷。0.13.2 实测:协调仓 ui-responsive 0.1.11 含悬浮球开关、apk 仓副本 0.1.9 无它(DevSection 少了 W7 悬浮球开关行 + 配置导入导出块);host-web-compat 0.1.8 vs 0.1.6(#120 拒绝信号缺席)。**教训:凡协调仓动了这三个独立子仓/bridge/manage,发布前必须比对两个仓库的 package.json version + 抽验 apk 仓副本 lib/client.js 关键字符串(如「悬浮球」),不一致即用 robocopy 从协调仓同步**(`robocopy "<协调仓子仓>" "<本仓\<同名子目录>" /E /XD .git node_modules /XF *.tgz`,lib/ 必须一并拷入——自包含链不对这些子仓执行 npm build)。 37. **快照刷新中途杀进程 → 看门狗拿半解压运行时拉引擎 → 用户 settings 被剪(2026-09-05 三重实锤)**:① refreshSnapshot 全量解压在模拟器实测 **~8 分钟**(44805 文件;流式逐文件覆盖——「bridge mtime 已新」≠ 完成,**唯一完成标志 = `.snapshot-fingerprint` 翻转新值 + `.dsh-backup` 消失**,中途抽验必误判);② 解压中 force-stop → 无闸门的看门狗 5s 一拍用「半新半旧运行时」spawn 引擎(12:45:26 补丁日志实证)→ 混合态引擎的 settings 归一化把 `llm-pi-ai.providers`(**用户自定义供应商 Hy3/opencode-go 挂此**)剪成出厂空模板,且后续刷新备份忠实保留损坏结果;③ 恢复通道 = `undo-snapshots/auto/<最后好版本>/home-settings.yaml` 拷回 `.dsh/settings.yaml` + 重启(.credentials.yaml 的 apiKeyEnv 引用未受损)。**修复**:`EngineManager.snapshotRefreshing` companion 级 @Volatile 闸门(MainActivity/EngineService 各持实例字段互不可见,同 STARTING CAS 道理),刷新期 startEngine 直接跳过、finally 清标志。**升级/排障铁律:装新包触发重解压期间,禁 force-stop、禁拔 USB、禁 adb reboot(协调仓雷点 1 同源)。** 38. **rc.2 锁定型运行时补丁在引擎升级时抹掉新引擎代码(0.13.3 模拟器实锤,prompt 链阻断)**:assets/patched 的 session-persistence-jsonl/attachment-local 等是 0.1.1-rc.2 原版整文件(无 delta),0.1.2-rc.1 引擎启动前 applyRuntimePatches 整文件覆盖 → 新代码被旧版回退(persistence.borrowSession is not a function → 一切会话写入/发送全断)。**修复/约定**:升级引擎时逐补丁核对上游是否原生覆盖(能退役则退役——fs-local/primitives 已退役);重出 asset 必须从新版本包文件改起(RUNTIME-PATCHES.md §3-2);本次重出 SPJ/ATT 的最小 delta = link(2) 失败 EACCES/EPERM/ENOTSUP → rename 回退(grep "rebuild-runtime-patches" 见协调仓 .tmp-upgrade 备忘)。grep `borrowSession`。 39. **rc.1 loader module table 不再应答 client-runtime require(store-rehome 落地)**:上游把 store 引擎迁到 @deepseek-ai/dsh-client-store(web dist 内联 + Module table 命名空间),ui-responsive 构建预设的 RUNTIME_STORE_EXEMPTION(require("@deepseek-ai/dsh-client-runtime/client") external)在新 loader 上必炸——"missed the module table — build-time externals drift",boot 即 Failed to load plugins。**修复**:源码 import 迁到 client-store;构建预设 CLIENT_EXTERNALS 移除豁免、INLINE_SAFE 加 client-store(内联,官方 ui-layout 同款);ClientContext 类型改用 cordis Context;自备 slots-augment.d.ts(SlotMap root / GlobalStandardProps.useSessions / Context.slots-sessions 增强抄自 runtime rc.2 types)。grep `store-rehome`。 40. **npm arborist 对复杂 peer 树 + 精确 pin 会崩(spec undefined)**:ui-responsive 升 cordis 4.0.2 + client-store rc.1 后,npm install 带包参数崩(Cannot read properties of undefined (reading 'spec'));且半装 node_modules 会让 "up to date" 谎报。**处置**:删 node_modules + package-lock 全新 install --legacy-peer-deps;peer cordis 从精确 4.0.1 放宽到 ^4.0.2;装完必须回读 node_modules//package.json 验证版本(npm "up to date" 不代表真装)。grep `legacy-peer-deps`。 41. **overlay 登记表漏项(primitives 缺席 rc.1 升级)**:engine-overlay.json 生成以 research 的 196 包清单为主循环——@deepseek-ai/dsh-client-ui-primitives 在旧树但不在清单 → 未覆盖留在 rc.2。**修复/约定**:登记表必须与基座树全量对账(.tmp-upgrade/audit-manifest.mjs:base-nm-paths 逐包 vs manifest ∪ keepUnpublished,未覆盖即查 npm);发现 npm 有新版即补登记+拉 tgz。grep `audit-manifest`。 42. **run-as 的权限视图不代表引擎运行时**(Android 15 模拟器实测):appops MANAGE_EXTERNAL_STORAGE allow 后 run-as cat /storage/emulated/0 仍 Permission denied(run-as/FUSE 评估差异)——引擎进程(同 uid 真实运行)实测可读(AI 轮 read 工具逐字读回)。验证权限问题必须走引擎运行时(会话轮/工具),不能只信 run-as。grep `run-as`。 43. **AppFrame 白屏静默挂起:create 循环 + rc.1 session-scope 严格绑定(0.13.3,两轮定位,已修)**: (原以「坑 43 修复记录」附录形态追加,2026-09-12 归位为编号条目,正文未改。) **第二轮定位(原「43 续」):白屏静默挂起的真因与修复**: 首轮定位到「create 循环静默挂起」后,用 **Runtime.enable + dist 插桩**(设备侧 python 给 index-Df-65__b.js 的 boot await 链插 console.log)拿到完整时序:**50 个 entries 全部 created、r5-pluginboot-done、mount-effect、r6-mounted 全部触达——boot 管线本身是通的**。真因在 mount 后的 **React 渲染错误**:`Error: strict session slot 'details' rendered without a scope binding`(console error,0.13.2 时代无此强制)。 **根因**:rc.1 对 session-scope 槽(details 等)加了**严格 scope binding 强制**——session-scope 槽必须包在框架注入的 `` 里渲染(官方 ui-layout AppFrame 的写法:`jsx(SessionProvider, { children: renderSlot("details", {}) })`,SessionProvider 从 AppFrameProps 解构)。我们 rc.2 时代的 AppFrame 直接裸渲染 `renderSlot('details')` → 渲染期 throw → React 卸载整树 → root 空、且 React 渲染错误不进 Runtime.consoleAPICalled(error boundary 前抛出)→ **零 console 静默白屏**。 **修复**:AppFrame.tsx 三处——① 解构加 `SessionProvider`;② 桌面形态 `DetailsColumn` 内包 `{renderSlot('details')}`;③ 移动形态 bottom sheet 同款。conversation 是 session-maybe scope(无需 binding);sidebar 是 root scope。 **定位方法论(可复用)**: 1. `Runtime.consoleAPICalled` 事件**必须先发 `Runtime.enable`** 才投递——没 enable 时「零 console」是探针假象(本坑第一轮误判「静默」的根因之一)。 2. 设备侧 python 给 minified bundle 插桩(boot await 链插 console.log)+ push + run-as(带全套引擎 env,坑 22)+ 重启引擎(内存中的旧 bundle 不会自动换)→ CDP reload 读日志——黑盒时序一步到位。 3. 服务器内存 serve:combo bundle 从 flush 时内存 responses 出(非磁盘直读),**改 dist 文件后必须重启引擎**才生效。 **验证**:SessionProvider 修复 + 第 6 次装机解压后——rootLen=411682、零 error、composer(contenteditable)在场、截图实证完整 UI(Hy3 选择器/会话历史/悬浮球全部在场)、session/create+list+prompt 新 wire 全 PASS。 44. **WSL 9p 挂载 chmod 无效 → 归档权限归一化只能在重打包层做(2026-09-08 实测)**:`/mnt/d` 是 9p 挂载且未启用 metadata,`chmod 600` 后 `stat` 仍 777、`tar -tvf` 记录 777(实测探针目录)。因此「归档前 chmod 整棵树」在 Windows 侧是**无效步骤**(白走 6 万文件),快照 tar 的权限只能靠**流式重打包**修正——唯一权威落点 = `scripts/inject-all.py`(重写每个成员:ELF/shebang=0700、数据文件=0600、目录=0700),门禁 `scripts/check-snapshot-file-modes.mjs` 校验注入后快照(= APK 内嵌 + 发布资产同源)。`build-snapshot-013.mjs` 8a3 步已删除并注明原因;`build-apk-013.ps1` 的 `-SkipInject` dev 档跳过该门禁(警告不拒打包)。grep `check-snapshot-file-modes`。 45. **快照含绝对符号链接(9 个 applet 指向 `files/usr/...`)→ 暂存解压必须放行 runtimeRoot 内的绝对目标(2026-09-08 实测)**:final 快照 1989 个符号链接中 554 个是绝对目标——545 个 Termux 残留(`/data/data/com.termux/...`,应拒)与 **9 个指向本应用运行时根**(`/data/user/0/com.dsharnessmobile.shell/files/usr/bin/{editor,ex,nc,pager,vi,view,vim,vimdiff,vimtutor}` → `libexec/{busybox,vim}/*`、`bin/more`;设备实测这 9 条在场)。旧解压器只在 `dest`(当时 = filesDir)内放行,**暂存目录解压(`.snapshot-stage`)会把它们全部静默丢弃** → applet 缺失。修复:`SnapshotExtractor.extract(..., runtimeRoot = context.filesDir)`,绝对目标只要落在 runtimeRoot 内即放行(交换后正是正确路径);相对链接仍须落在解压根内,Termux 残留与 `../` 逃逸照旧拒绝。在线更新路径(UpdateManager,dest=update-stage)同款受益——此前同样在静默丢链。回归验证:装机后 `run-as ... ls -l files/usr/bin/more` 应见绝对链接。grep `isLinkTargetAllowed`。 46. **无障碍通道的两条「僵尸」陷阱(2026-09-10 模拟器实测)**:① **prefs 僵尸 a11yEnabled**——`am force-stop` 杀进程时 `onDestroy/onUnbind` 不保证执行,`dsh-adb.xml` 里的 `a11yEnabled=true` 会留在原地,而系统已解绑服务;引擎若只读该标记就会把请求投进队列后无人取活(表现为工具 8s 超时)。修复:引擎侧 `a11yEnabled()` = prefs 标记 **且** 队列轮询心跳新鲜(`ControlQueue.pollAgeMs() < 20s`,壳侧长轮询每次调用即刷新 `lastTakeAt`)。② **重启后服务不解绑但也不重连**——force-stop + 重新 `am start` 后必须重新 `settings put secure enabled_accessibility_services ...`(实测 `settings get` 返回 null),否则 `dumpsys accessibility` 的 `Bound services:{}` 为空。排障顺序:`dumpsys accessibility | grep -A2 "Bound services"` → prefs → 队列 `lastTakeAt`。grep `pollAgeMs`。 47. **门禁顺序错位会让新通道永远不可达(用户当场指出的设计缺陷,2026-09-10)**:把无障碍后端接进工具层后,如果 `gateFor()` 仍然只认 ADB 三道人门,工具会先被 ADB 门拒绝——a11y 分支永远走不到(实测:AI 拿到的一律是「请在开发者选项 → 无线调试 配对」)。修复原则(PRD §3.3 B3):**两条通道等价、无障碍优先**——`gateFor` = a11y 在线 **或** ADB 门齐备,`ControlPolicy.decideControl(op)` 再决定后端;会话档位 `danger-full-access` 对两者同等要求。设置页也必须同步(无障碍为主入口 + 官方 Intent 跳系统设置 + Android 13 受限设置一键解锁,ADB 折叠为高级/脚本面),否则「改了后端没改授权面」等于没改。grep `decideControl`。 48. **`settings.describe(options)` 的 options 被实现忽略(0.13.5 引擎侧踩坑,影响所有读其它命名空间的插件)**:`ctx.settings.describe({namespaces:['llm-pi-ai']})` **不会**按命名空间过滤,返回全部注册命名空间且顺序即注册顺序——取 `[0]` 很可能拿到 `llm-deepseek`,于是自定义路由永远查不到(表现为能力自动补全静默不写回)。正确写法:`.find(d => d.ns === 'llm-pi-ai')`。同族坑:未 `inject` 的服务直接读属性会抛 `cannot get property "credentials" without inject`——可选服务一律走 `ctx.get(name)`。grep `describe(options)`。 49. **inset 通道只做了一半:没有 top 通道 → 关闭沉浸式后顶栏/设置页头被状态栏压住(#135,2026-09-10 模拟器实证)**:`WindowCompat.setDecorFitsSystemWindows(window, false)` 让页面铺满全屏,但 `MainActivity` 的 insets 监听**只缓存 bottom/mandatoryGestures/ime 并推给页面**,`bars.top` 仅用于引导页 padding;同时 Android WebView 实测 `env(safe-area-inset-top) = 0`,于是状态栏可见时(沉浸式关闭)topbar 矩形 `y=0 h=61` 的顶部 16px 与设置面板 nav(`y=12`)都落在状态栏(48 物理 px = 24 CSS px)下面。修复:insets 监听补 `webSystemTopInset = pxToCssPx(max(bars.top, displayCutout.top))` → `pushWebInsets` 推 `--dsh-android-system-top`(状态栏隐藏时 `bars.top=0`,开关天然自洽);注入层 `.mobileFrame` 定义 `--dsh-mobile-top-inset = max(env(safe-area-inset-top), var(--dsh-android-system-top))`,topbar/抽屉/设置面板/轨迹面板/开发者弹层消费。注意设置面板是被 fixed 遮罩**居中**的,改高度会把头部推回状态栏下——正确做法是 `box-sizing: border-box` + `padding-top`。grep `webSystemTopInset`。 50. **弹出面板几何:宽度上限打在内层滚动容器上 = 卡片留空条 + 滚动条悬空(#135,CDP 实测)**:斜杠菜单 DOM 是「卡片 `[class*=_menu]`(背景/圆角/阴影)> 滚动容器 `[role=listbox]`」,壳侧历史注入脚本的 `[role="listbox"],[role="menu"]{max-width:min(92vw,340px)!important}` 只命中内层 → 卡片仍 410px、内容 340px(实测 `x=16 w=410` vs `x=20 w=340`),右侧 70px 空条、滚动条落在离卡片右缘 70px 处。模型菜单另有 `right:0 + width:max-content`,以触发器为基准 → 360px 视口下左缘 -84px,模型名丢前缀。修复:注入层 `ComposerPopupGuard` 按**实测矩形**写 `--dsh-mobile-popup-max-width`(卡片与滚动容器同值)+ `--dsh-mobile-popup-shift`(水平钳制,`[data-dsh-popup]` 上 translateX)+ 高度上限;按 `data-*` 锚点与测量工作,上游 CSS Module 改名不回归。grep `ComposerPopupGuard`。 51. **无障碍截屏落应用私有目录 → 引擎 read_image 打不开;且多花一轮(#127,2026-09-10)**:`DeviceControlService.handleScreenshot` 原写 `filesDir/control-shots/`,该目录不在引擎可读根内(引擎侧报 `EACCES: open '/data/user/0'`)。修复:改写 `files/home/tmp/dsh-tmp/`(= `EngineManager` 给引擎的 `TMPDIR`,与管理插件 ADB 截图落地同源)+ 目录 LRU 兜底保留 8 份;工具层 `android_screenshot` 读字节 → `attachments.saveImage` → **结果内联图像块 → 立即删除临时文件**(模型不再需要额外一轮 `read_image`,零残留);路由不支持图像/附件缺失/超上限时回退返回路径。grep `inlineShot`。 52. **点击无生效校验(#129)→ 新增便宜 `state` op**:`AccessibilityService` 的窗口/内容/滚动事件已维护 `invalidated` 标记与快照代次 `generation`,但只在 dump 时暴露。修复:新增 `state` op 返回 `{gen, invalidated, enabled}`(**不建树**),`android_ui_click` 点后 260ms 读一次并回报「已生效/未观察到变化」;同时把 `ACTION_CLICK` 的节点中心 / 手势落点回填到 `x/y`(此前 a11y 归一化路径恒返回 0)。新增 op 必须同时进引擎侧 `ControlOp` 与 `A11Y_OPS`(坑 47 同族)。grep `handleState`。 53. **悬浮球完成态脱节(#133)**:`api-session/status running=false` 只重置 `sessionBusy/toolCount`,**不清该会话的待答/待审批项** → `pendingKind` 仍派生为 question/approval,球停在琥珀「等待你的回答…」且面板不收。修复:完成事件调用 `OverlayPanel.dropPendingFor(agentId)` 丢弃该会话 pending,并按 `overlay_display/auto_collapse_on_done`(默认开)自动收起面板——**有未提交草稿时不收**。grep `dropPendingFor`。 54. **目录同名模型跨厂商方言不一致 → 写入 reasoningEfforts 会让请求被网关拒(#134,独立排查)**:`dsh-model-capability` 从引擎目录按模型 id 取 `thinkingLevelMap` 写 `reasoningEfforts`,但**不写配套的 `compat.thinkingFormat`**;pi-ai 便按探测默认方言(未知 baseURL → `openai`)发送 `reasoning_effort`,真实厂商方言(如 zai)不同的网关可能直接 400,且档位持久化在 `agent-default-model` 被新会话继承。修复(0.2.1):方言键改用**严格口径**——只要有目录声明了而另一些没声明即视为「方言不明」→ 跳过 `reasoningEfforts` 写入并记冲突;统一时连同 `compat.thinkingFormat/supportsReasoningEffort/maxTokensField` 一起写。grep `pickDialect`。 55. **历史编号保留(内容不可还原)**:该编号由 0.13.5/0.13.6 的更新记录行引用("新增坑 55-57"),但正文从未落到本文件;`docs/AGENTS/changelog-archive.md` 现存最早条目仅到 0.13.2-preview(2026-08-31),无法还原。按"不重编号"纪律保留该号占位; 如需补正文,从 0.13.6 的认证台账 `docs/AGENTS/0.13.6-CERTIFICATION.md` 与当期 PR 描述回溯(登记义务:找到即回填本条)。 56. **历史编号保留(内容不可还原)**:同坑 55——0.13.5/0.13.6 更新记录行引用的编号,正文不在库内、archive 无对应版本行。保留占位,勿重编号。 57. **历史编号保留(内容不可还原)**:同坑 55——0.13.5/0.13.6 更新记录行引用的编号,正文不在库内、archive 无对应版本行。保留占位,勿重编号。 58. **0.1.5 起 ui-layout 不能禁用(2026-09-10,追上游)**:上游把 `ui-layout` 变成布局服务中枢(`ctx.layout` 五方法、键控 `main` 槽、`provideRoot({hooks:{panelInfo}})`、`layoutInfo` 八字段)。profile patch 若仍 `- id: ui-layout / disabled: true`(0.1.2 时代为换自研 AppFrame 而设),`ui-conversation` 注册不进 `main`、`ui-sidebar-right` 注入不到 `rightbar`、`SidebarRoot` 读不到 `usePanelInfo` → **会话与左栏一起消失**。修复:删除该 disabled 条目,注入层改「移动适配层」(0.2.0 起不再注册 root 槽、也不再 `provide('layout')`——重复 provide 会整链失败,同 directory-picker 事故)。 59. **手机形态不能再锚 `[data-mobile]`(注入层 0.2.0)**:旧 AppFrame fork 自带 `[data-mobile]`/`[data-mobile-topbar]`;去 fork 后这些属性不复存在,样式会静默失效(弹出面板越界、设置页窄条、轨迹详情被遮)。现由 `mobile/form-marker.ts` 发布 `html[data-dsh-mobile-form]`(镜像 `(max-width:767px)`)、`[data-dsh-frame]`(由上游 `[data-rightbar-col]` 反查)、`html[data-dsh-modal-open]`、`[data-dsh-settings-dialog]`;顶栏为 `[data-dsh-mobile-topbar]`。改动样式前先 grep 这五个锚点。 60. **原生「打开方式」两条出口与白名单(0.13.7)**:`PathOpen.openChooser` 与 `FileIncoming.openWithExternalReader` 共用 `FileIncoming.isReaderAllowed`(file_paths.xml 同一映射面);目录主候选固定 `ACTION_OPEN_DOCUMENT_TREE`(FileProvider 目录 URI 多数文件管理器不可枚举),MT 管理器等按包名进「初始意图」——装了才出现,未装不臆造。返回 `{"ok",reason?}`,页面按 reason 分流文案,绝不静默(`no-handler` 弹提示)。 61. **polyfill 片段共用一个 `