dsh-cindy-host 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +462 -0
- package/LICENSE +21 -0
- package/README.md +178 -0
- package/cordis.patch.yml +6 -0
- package/doc/architecture.md +34 -0
- package/doc/cindy-android-verify.md +362 -0
- package/doc/cindy-mobile-freeze-handoff.md +124 -0
- package/doc/cindy-phone-link.md +2902 -0
- package/doc/desktop-runtime-fix-20260930.md +69 -0
- package/doc/development-plan.md +19 -0
- package/doc/file-peer-apply-recovery-20260921.md +35 -0
- package/doc/file-peer-installed-hotfix-20260921.md +53 -0
- package/doc/file-peer-minimal-apply-20260921.md +78 -0
- package/doc/file-peer-original-contract-alignment-20260921.md +119 -0
- package/doc/file-peer-phone-export-observation-20260921.md +75 -0
- package/doc/file-peer-product-contract-audit-20260921.md +106 -0
- package/doc/file-peer-root-cause-20260921.md +53 -0
- package/doc/file-transfer.md +50 -0
- package/doc/host-performance-20260930.md +91 -0
- package/doc/neonabyss-apk-file-peer-diagnosis-20260921.md +87 -0
- package/doc/new-session-sync-fix-20260930.md +59 -0
- package/doc/patches/cindy-peer-silence-probe.patch +1112 -0
- package/doc/projection-read-probe.md +77 -0
- package/doc/publishing.md +259 -0
- package/doc/releasing.md +149 -0
- package/doc/research.md +24 -0
- package/doc/session-discovery-2026-09-20.md +67 -0
- package/lib/client.js +763 -0
- package/package.json +84 -0
- package/src/auth-session.js +189 -0
- package/src/authorization-policy.js +59 -0
- package/src/cindy-channels.js +1707 -0
- package/src/cindy-login-flow.js +159 -0
- package/src/cindy-login.js +80 -0
- package/src/cindy-message-row.js +432 -0
- package/src/cindy-session-row.js +171 -0
- package/src/console-sink.js +6 -0
- package/src/contracts.js +26 -0
- package/src/credential-store.js +128 -0
- package/src/demo.js +9 -0
- package/src/dsh-active-sessions.js +59 -0
- package/src/dsh-host-source.js +62 -0
- package/src/dsh-message-fold.js +608 -0
- package/src/dsh-plugin.js +1932 -0
- package/src/dsh-session-source.js +529 -0
- package/src/file-peer-protocol.js +7 -0
- package/src/file-peer-rtc.js +41 -0
- package/src/fixture-source.js +13 -0
- package/src/host-approvals.js +329 -0
- package/src/host-attachments.js +249 -0
- package/src/host-authorization.js +51 -0
- package/src/host-demo.js +3 -0
- package/src/host-export-jobs.js +356 -0
- package/src/host-export-owner.js +62 -0
- package/src/host-file-browser.js +400 -0
- package/src/host-file-ice.js +63 -0
- package/src/host-file-peer.js +289 -0
- package/src/host-file-reference.js +16 -0
- package/src/host-files.js +148 -0
- package/src/host-frame-budget.js +310 -0
- package/src/host-goals.js +284 -0
- package/src/host-history-view-limits.js +22 -0
- package/src/host-history-view.js +417 -0
- package/src/host-input-queue.js +505 -0
- package/src/host-media-fetch.js +511 -0
- package/src/host-media.js +678 -0
- package/src/host-models.js +231 -0
- package/src/host-palette.js +111 -0
- package/src/host-push-channels.js +37 -0
- package/src/host-reconnect.js +54 -0
- package/src/host-routes.js +266 -0
- package/src/host-settings.js +41 -0
- package/src/host-status.js +216 -0
- package/src/host-transfer-diagnostics.js +50 -0
- package/src/host.js +2497 -0
- package/src/login.js +31 -0
- package/src/logout.js +3 -0
- package/src/projection-read-model-sink.js +6 -0
- package/src/projection.js +43 -0
- package/src/session-flags.js +160 -0
- package/src/session-publisher.js +123 -0
- package/src/session-read-model.js +6 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# 交接:Cindy 手机端「切后台回来卡死」的调查与现状
|
|
2
|
+
|
|
3
|
+
> 2026-09-18 夜间整理。给**新开的对话**用:读完这一份即可接手,不需要读旧对话。
|
|
4
|
+
> 工作目录 `G:\Projects\DSH-cindy-host`(Host 插件);客户端仓库 `G:\Projects\Cindy`(分支 `fix/mobile-peer-silence-probe`)。
|
|
5
|
+
> 详细过程与逐条证据见:`doc/cindy-android-verify.md`(第九/十/十一节)与 `.claw/truth/dsh-cindy-host-mobile-resume-limitation.md`。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1. 一句话现状
|
|
10
|
+
|
|
11
|
+
**根因已定位:App 从后台回到前台后,RN(Android)的 `setTimeout`/`setInterval` 定时器驱动整体死亡。**
|
|
12
|
+
它在 JS 侧救不回来(下面第 5 节全是实测结论),唯一有效动作是重启 App —— 而**在"内嵌 bundle 的 release 构建"里没有可用的 JS 重启入口**(`Updates.reloadAsync()` 会被原生层拒绝/崩溃)。
|
|
13
|
+
**当前给用户的可用版本是 `83d85c547`(能用、不会崩、卡了要手动重开)。**
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## 2. 现象与可观测判据
|
|
18
|
+
|
|
19
|
+
**用户可见**:切后台 1–2 分钟回前台后,「思考时间」不再走、消息不刷新;退到会话列表再进来只更新一次;**杀掉 App 重开**才彻底恢复。
|
|
20
|
+
|
|
21
|
+
**Host 侧(不看手机就能判)**:
|
|
22
|
+
|
|
23
|
+
| 观测 | 含义 |
|
|
24
|
+
|---|---|
|
|
25
|
+
| `tdiag.stall.s<秒>.r<次数>.frame` | 手机自报:入站帧发现计时器停摆(渠道名即数据,见第 6 节) |
|
|
26
|
+
| `tdiag.sched.t?i?r?` | 停摆时探测哪个调度器还活着。**实测值 `t0i1r0`**:`setTimeout` 死、`setImmediate` 活、`rAF` 没来 |
|
|
27
|
+
| 停摆窗口内**零请求** | 客户端心跳/重连/重试全建在 `setTimeout` 上 → 永不重连(Host 实测过一次连续 90 秒) |
|
|
28
|
+
| 恢复时刻的 `device-link:subscribe` + 全套重读爆发 | 冷启动(或手动重开)的签名 |
|
|
29
|
+
|
|
30
|
+
读取工具:`node tools/verify-timer-stall.mjs`(一次打印自报、调用空档、连接层与判读)。
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 3. 根因链(证据在代码里)
|
|
35
|
+
|
|
36
|
+
1. `node_modules/react-native/Libraries/Core/setUpTimers.js:17` —— `setTimeout`/`setInterval` 由 **`JSTimers` 原生模块**提供(平台不再投递定时回调 → 整体停摆)。
|
|
37
|
+
2. 同文件 `:97` + `Timers/immediateShim.js:21` —— `setImmediate` 建在 **`queueMicrotask`** 上,所以停摆时它仍工作(实测 `i1`)。
|
|
38
|
+
3. `packages/device-link/src/client.ts` —— `pingTimer`(`setInterval`)、`reconnectTimer`、`peer.retryTimer` 全用全局定时器 → **停摆后客户端永不重连**。
|
|
39
|
+
4. `apps/mobile/app/sessions/[sessionId].tsx` —— 「思考时间」是 `setInterval(updateElapsed, 1000)`。
|
|
40
|
+
5. `packages/maker-shared/src/historyViewController.ts` + `apps/mobile/src/session/remoteSessionStore.ts` —— 视图失效与流式增量 flush 都压在 `setTimeout` 上。
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 4. 已经做过什么(逐层结论)
|
|
45
|
+
|
|
46
|
+
| 层 | 内容 | 真机结果 |
|
|
47
|
+
|---|---|---|
|
|
48
|
+
| 1 | `timerLiveness` 守卫:入站帧/回前台为检查点,发现停摆就重建心跳 + 上报 | **检测有效**(`tdiag.stall` 稳定出现);**重建无效**(间隙单调增长 81→103→109→…→125,`reviveCount` 停在 1,**从无 recover**) |
|
|
49
|
+
| 2 | `HistoryViewController.invalidate` 的 500ms 防抖 → 停摆时改挂钟节流直读 | 有效(数据层) |
|
|
50
|
+
| 3 | `remoteSessionStore.scheduleTextDeltaFlush` → 停摆时直接 flush | 有效(数据层) |
|
|
51
|
+
| 4 | 停摆时强制 `connectionEpoch` 换代(等价"退出去再进来") | 有效(数据层) |
|
|
52
|
+
| 5a | **垫片**:包住全局 `setTimeout`/`setInterval`,用 `setImmediate` 接管 | ❌ **App 卡在 splash**。已回退(`a8b80a893`)。教训:单测只跑了"注入假定时器",**真实全局包装的启动路径从未验证** |
|
|
53
|
+
| 5b | **自动重启**:连续两次确认停摆 → `Updates.reloadAsync()` | ❌ **切出去回来就崩**。原因:`reloadAsync` 只对"有 OTA update 被加载"的构建有效,内嵌 bundle 的 release 会被原生层拒绝/崩(仓库自己 `manualUpdateCheck.test.ts` 注释写过)。已停用(`710782b8e`) |
|
|
54
|
+
|
|
55
|
+
**数据层修复 2/3/4 有效但用户看不见** —— 屏幕仍然不更新,因为渲染/定时器那一层没救。
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## 5. 不要重复的坑(全是实测)
|
|
60
|
+
|
|
61
|
+
1. **JS 救不活计时器**:新建 `setInterval` 在停摆态同样不触发(这是判据 `t0i…` 的直接含义)。
|
|
62
|
+
2. **别包全局定时器**:会在启动路径上崩(5a)。
|
|
63
|
+
3. **这个构建里没有可用的 JS 重启入口**:`reloadAsync` 崩;`DevSettings.reload` 只在 debug;要重启必须加**原生**模块。
|
|
64
|
+
4. **别把"回前台"当故障证据**:后台待多久,心跳就"像停了多久"。必须要求**连续两次检查仍停摆**(真死会继续涨,后台假死会在下一个入站帧上消失)。这条已经做进 `timerLiveness.onStallPersisting`。
|
|
65
|
+
5. **重启类动作必须先落盘记账**:仓库 `apps/mobile/src/update/otaReloadGuard.ts` 就是为"某台设备累计重启 33 次仍在循环"写的;写不进存储就不许重启。
|
|
66
|
+
6. **判据要能在没有入站帧时送出**:上报若只在"检查点"收口,停摆期间可能永远发不出去;回前台是原生事件,必须在那里无条件收口(`flushSchedulerProbe`)。
|
|
67
|
+
7. 出包脚本会过滤 Gradle 输出,**失败原因行会丢**;脚本现已自动停 Gradle 守护进程 + 清库产物并在失败时打印原因(见 `tools/build-cindy-verify-apk.ps1`)。
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
## 6. 协议小抄:手机如何"无日志上报"
|
|
72
|
+
|
|
73
|
+
手机端没有落盘日志。自报方式是 **invoke 一个未知通道名**,Host 以 `CHANNEL_NOT_ALLOWED` 记下通道名与到达时刻:
|
|
74
|
+
|
|
75
|
+
- `psdiag.s<静默秒桶>.p<pending>.a<短超时在等>.c<累计沉默>.o<对端被标离线>`(device-link 侧对端静默)
|
|
76
|
+
- `tdiag.stall.s<秒>.r<重建次数>.<来源>` / `tdiag.recover.after<秒>s.r<次数>.<来源>`
|
|
77
|
+
- `tdiag.sched.t?i?r?[.<来源>]`(0/1 = 该调度器是否回调过;来源 `frame` 或 `active`)
|
|
78
|
+
- `tdiag.reload.*`(attempt/blocked/noguard/failed —— 注意:重启会把这些还没发出的上报一起带走)
|
|
79
|
+
|
|
80
|
+
Host 侧读取:`diagnostics.phoneDiagnostics`(0.1.7 起,带到达时刻、不被普通轮询冲掉)。
|
|
81
|
+
|
|
82
|
+
---
|
|
83
|
+
|
|
84
|
+
## 7. 未走的路(唯一剩下的方向)
|
|
85
|
+
|
|
86
|
+
**原生侧方案**,需要用户同意后另开工作项:
|
|
87
|
+
|
|
88
|
+
- 候选 A:加原生重启模块(如 `react-native-restart`),在"连续两次确认停摆"后调用;必须先落盘闸门。
|
|
89
|
+
- 候选 B:原生侧在 `onHostResume` 重新注册 RN 的 Timing/Choreographer 驱动(治本,但要动 RN 集成)。
|
|
90
|
+
- 候选 C:降低后台驻留期间的动作面,减少触发概率(治标,不修复已进入的状态)。
|
|
91
|
+
|
|
92
|
+
**无论哪个,验证要求(吸取 5a/5b 的教训)**:先在**本地**验证"启动安全 + 崩溃安全"(至少:真机冷启动、切后台回来、连续多次),再交给用户;不要再拿用户手机当第一次运行环境。
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## 8. 资产与索引
|
|
97
|
+
|
|
98
|
+
- 客户端提交(未 push,分支 `fix/mobile-peer-silence-probe`):
|
|
99
|
+
`eadedb8d5`→`308b4badc`→`bd4ed7e73`→`b72f15a61`→`94f083331`→`04d36977f`→`d4969890c`→`4b200d931`→`83d85c547`→`c3b49e0b1`→`988830d21`→`37fb34db9`→`a8b80a893`(回退垫片)→`10329b4c1`(自动重启)→`710782b8e`(停用自动重启)。
|
|
100
|
+
- Host 插件提交:`7025127` `fc5d320`(0.1.5)`c728f11` `3169cdd` `d1bb697`(0.1.7)`97f31c3`(0.1.8)`ac4e303`(0.1.9,401 自动刷新)`b303b85` `9bc49c1` `05070a6`(重启不弹窗,但见坑:那次改动让 WMI 路径失败过,已还原关键部分)`3112425` `8743fa4` `c939e6c`。
|
|
101
|
+
- APK(项目内路径,用户只用这个目录):`artifacts\Cindy-Verify-0.1.0-arm64-v8a-<commit>.apk`
|
|
102
|
+
- **可用**:`83d85c547`(sha256 `F6BC31D6…CEB7`)
|
|
103
|
+
- 会崩:**`10329b4c1`**(自动重启)、`37fb34db9`(垫片) —— 不要给用户
|
|
104
|
+
- 工具:`tools/export-download-link.mjs`(`--direct` 绕开 Host 陈旧凭据)、`tools/verify-timer-stall.mjs`、`tools/probe-oss-ext.mjs`、`tools/build-cindy-verify-apk.ps1`
|
|
105
|
+
- 文档:`doc/cindy-android-verify.md`、`.claw/truth/dsh-cindy-host-mobile-resume-limitation.md`、`.claw/truth/dsh-cindy-host-media-staging.md`
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## 9. 顺带已修好、与本案无关但别改坏的东西
|
|
110
|
+
|
|
111
|
+
- **阿里云 OSS 拒发安装包**:`.apk`/`.ipa` 后缀或 APK 的 Content-Type 会被 `400 ApkDownloadForbidden` 拦;Host 现在按不透明字节 staging(0.1.6)。
|
|
112
|
+
- **Host 列表读超时让手机判"设备离线"**:`local-db:sessions:list` 现在按缓存应答 + 后台刷新(0.1.8)。
|
|
113
|
+
- **Host 凭据轮换后 401**:媒体路径 401 会串行刷新并重试一次(0.1.9);另有 `--direct` 工具用于应急。
|
|
114
|
+
- **重启弹控制台窗口**:`windowsHide` 已加;但**不要把看守进程再套一层隐藏包装** —— 那会让 WMI 失败、退化成活不过命令的 spawn,重启静默不发生(实测踩过)。
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
## 10. 验收判据(修正版,别再用旧的那条)
|
|
119
|
+
|
|
120
|
+
旧判据里「Host 收到**恢复**诊断」**不可满足** —— JS 救不活计时器,`tdiag.recover.*` 永远不会出现。可判定的三条:
|
|
121
|
+
|
|
122
|
+
1. **用户可见**:停在会话页时思考时间自己在走、新内容自己进来(不需要退出去再进来);
|
|
123
|
+
2. 停摆窗口内手机**仍在持续读取**(说明 2/3/4 层生效);
|
|
124
|
+
3. 停摆被手机自己发现并上报(`tdiag.stall.*`),且随后出现一次**重新订阅**(层 4 换代生效)。
|