docs(05): 锚点补记二十九 —— W51 合并、S2/S7① 设备验收结论,并更正补记十九里 S7 那句写错的前提

改了什么:
- 新增锚点补记二十九:W51 做了什么(S2 的纯判据与「首屏从没成功过」这个判据为什么这么定、S7① 的
  GoneSessions、S7② 真正的缺口是「出路不能依赖网络」);设备验收结论(S2 通过:恢复网络后客户端自己
  补拉并显示出消息;S7① 通过:删掉会话后真踢一次 WS,重连窗口里没有注定 404 的补拉,而握手证明通路
  活着);以及 S3 / S7② **未验**(如实写明,不写成「没问题」)。
- 更正补记十九里 S7 那句「`sessionGone` 位没有消费者」:消费者一直都在(ChatScreen.kt:165-166 →
  ChatHomeScreen.kt:109 的 onSessionGone),由 b5c0b13(2026-10-04、该行所依据提交 ff699f5 的祖先)
  引入,已用 git merge-base --is-ancestor 验证。这句是我写错的,W51 的任务书因此也带着错前提。
- 记下这次同类教训(第二次):真机「没生效」时**先核设备上 APK 的 lastUpdateTime 与本地 APK 的时间**
  —— 我就是拿上一版 APK 测了半天;另记「撤 adb reverse 不会掐断已建立连接,要真抖 WS 用假后端的
  /__control/ws-close」。
This commit is contained in:
AskaEth committed 2026-10-06 09:20:40 +08:00
1 parent 620a4e77fa
commit b604fefbd4
1 file changed
+27
+27
View File
@@ -1266,6 +1266,7 @@ curl -s -X PATCH "$BASE/api/v1/sessions/$SID" -H "Authorization: Bearer $TOK" \
> 证据:`SessionsViewModel.kt:359-369`(`message_persisted` 只在 `sessionId == activeSessionId` 时推进)、列表更新唯一触发点 `ChatHomeScreen.kt:96-102`;**后端设计里已登记为遗留**:`Cyrene_v2/DevDocs/design/16:103`。归类理由:`message_persisted` **不带 `unreadCount`** ⇒ 客户端算不准(契约面缺增量);客户端可自愈(节流重拉 `GET /sessions`)。 > 证据:`SessionsViewModel.kt:359-369`(`message_persisted` 只在 `sessionId == activeSessionId` 时推进)、列表更新唯一触发点 `ChatHomeScreen.kt:96-102`;**后端设计里已登记为遗留**:`Cyrene_v2/DevDocs/design/16:103`。归类理由:`message_persisted` **不带 `unreadCount`** ⇒ 客户端算不准(契约面缺增量);客户端可自愈(节流重拉 `GET /sessions`)。
> >
> **S7 · 客户端问题 · 仅技术债**:**已删除会话的僵尸 VM** 下次重连会发一次注定 404 的补拉,404 被吞(`sessionGone` 位没有消费者)。证据:`SessionsViewModel.kt:265-278`、`ChatViewModel.kt:2303-2310`/`:2333-2336`/`:2912-2914`。 > **S7 · 客户端问题 · 仅技术债**:**已删除会话的僵尸 VM** 下次重连会发一次注定 404 的补拉,404 被吞(`sessionGone` 位没有消费者)。证据:`SessionsViewModel.kt:265-278`、`ChatViewModel.kt:2303-2310`/`:2333-2336`/`:2912-2914`。
> ⚠️ **本行有一处我写错了(W51 更正)**:`sessionGone` 的**消费者一直都在** —— `ChatScreen.kt:165-166` 的 `LaunchedEffect(state.sessionGone)` → `ChatHomeScreen.kt:109` 的 `onSessionGone`(一句如实告知 + 带回主会话),由 **`b5c0b13`(2026-10-04,本行所依据提交 `ff699f5` 的祖先)** 引入,`git merge-base --is-ancestor` 已验证。**真正缺的那一环**是「出路不能依赖网络」(原来只靠 `refresh` 成功后的回落),见锚点补记二十九。
> >
> **S8 · 结论:以下几处主/次「没有分叉」,别再返工**:发送/流式/灰态/**并行分桶**、分页与补拉、主会话禁删(三处一致)、改名/置顶、已读游标、分享落地/D2 搜索/图片与附件 —— 全部按 `sessionId` 等值收敛,**同一份代码**。 > **S8 · 结论:以下几处主/次「没有分叉」,别再返工**:发送/流式/灰态/**并行分桶**、分页与补拉、主会话禁删(三处一致)、改名/置顶、已读游标、分享落地/D2 搜索/图片与附件 —— 全部按 `sessionId` 等值收敛,**同一份代码**。
> 另:**`sessionId → N` 序号映射已在 `c09a211` 整块退役**(`devdocs/02:146`)⇒「次级会话序号」**不再是待办**。 > 另:**`sessionId → N` 序号映射已在 `c09a211` 整块退役**(`devdocs/02:146`)⇒「次级会话序号」**不再是待办**。
@@ -1542,3 +1543,29 @@ curl -s -X PATCH "$BASE/api/v1/sessions/$SID" -H "Authorization: Bearer $TOK" \
- 还在等/待决策的:**等后端** `toolCalls[]` 回看面(要带输出)+ `unreadCount` 增量; - 还在等/待决策的:**等后端** `toolCalls[]` 回看面(要带输出)+ `unreadCount` 增量;
**待主人**:登出条目观感、移除键位置观感、相册多选分享真机、推送(等自建); **待主人**:登出条目观感、移除键位置观感、相册多选分享真机、推送(等自建);
**待主人拍板**:S1 第二步(回收旧 VM)、S5(切会话打断/停止入口)、W50 那条窄尾巴(降级气泡按 `proactive-local:` 前缀屏蔽删除入口)。 **待主人拍板**:S1 第二步(回收旧 VM)、S5(切会话打断/停止入口)、W50 那条窄尾巴(降级气泡按 `proactive-local:` 前缀屏蔽删除入口)。
---
## ★★ 锚点补记二十九 · W51(S2 + S7 次级会话补拉安全网)合并、**S2 设备验收通过**
> 合并提交 **`620a4e7`**(`android` 已推);分支/worktree `Cyrene-W51` 已清。全量真跑:**93 suite · 1205 tests · 0 失败 · 0 error**(基线 1193,+12)。
### 1. 做了什么(每条都写清「为什么」在代码 KDoc 与提交信息里)
- **S2**:新增纯判据 `ui/chat/ReconnectPull.kt`(`Forward / FirstPage / Nothing`,四级,只看 会话 id + 首屏状态 + 掉线游标 + 已知它没了);`ChatViewModel` 把首屏 `loadLatest` 提成 `loadFirstPage()`,**init 与补拉共用一份**,并新增 `FirstPageState`。判据刻意用「**首屏从没成功过**」而不是「本端有没有数据」——服务端真的没消息时列表也是空的,拿空列表当判据 = 每个空次级会话每次重连整表重拉(S4/S7 的成因)。`Loading` 不算失败(S3 那条时序一个字节没动);**主会话排除在外**(它有 `history` 兜底,再补一条 REST 就是第二套口径)。
- **S7①**:新增 `core/session/GoneSessions.kt`(进程级、不持久化、登出清空)+ `AppContainer.goneSessions`;写入口是 `SessionsViewModel.deleteSession` 成功那一刻,读入口是每个 `ChatViewModel`。判据第 1 条拦掉注定 404 的补拉:本地删过的**零次**,已 404 过的从「每次重连撞一次」收敛成**恰好一次**。
- **S7②**:⚠️ **任务书的前提是错的** —— 消费者一直都在(见补记十九那行的更正)。真正缺的是「出路不能依赖网络」:原来只靠 `refresh` **成功**后的回落 ⇒ 网络一抖,「已切回主会话」就是一句做不到的话。现在 `ChatHomeScreen.onSessionGone` **先在本端** `select(主会话)`(与用户点抽屉那一行同一条路),`refresh` 退回本职。
### 2. 设备验收(`emulator-5554` + 假后端 18090,App 地址 `localhost`、`adb reverse tcp:80`)
- **S2 通过**:先在线进次级会话(判据:客户端发 `POST …/read`)→ 撤 reverse 冷启动(离线,屏幕 `还没有消息` + `无法连接到服务器` + `重连中…`,注入的那条服务端消息在屏幕上 **0 次**)→ 装回 reverse → **20 秒处客户端自己发出 `GET /ws/chat` 紧跟 `GET /api/v1/sessions/web:5eed…/messages`**(就是那条补拉),**屏幕上的消息自己出现(1 次)**,全程没碰一下。
- **S7① 通过(否定式证据)**:在抽屉里删掉那个次级会话(服务端列表随即只剩 `web`)→ 用假后端 `POST /__control/ws-close` **真踢**一次 WS(握手日志显示 App 在 `09:20:01` 重连回来)→ 重连窗口里**没有任何该已删会话的 `…/messages` 请求**(`web:5eed` 计数 0),而**握手本身证明同一窗口通路是活的**(可信的否定)。反事实(不加闸就会撞一次)由补记十九的登记证据 + `ReconnectPullTest` 的单元测试承担。
- **S3(冷启动闪「还没有消息」)**:**未验**。W51 刻意没动那条时序;dump 采样的粒度抓不到亚秒级闪烁,我没有证据,不写成「没问题」。
- **S7②(在别处被删 ⇒ 被带回主会话且不依赖网络)**:**未验**(要造「别处删除」的场景)。代码路径与副作用(给主会话发一次 `POST …/read`,与用户手点主会话逐字相同)已在提交信息里写明,等主人复看。
### 3. 这次我又一次踩在自己的测量上(第二次同类,写下来)
S2 我第一次测出「没生效」,差点去改代码——真因是**设备上跑的还是上一版 APK**(换图标那版,`lastUpdateTime` 00:29,而含 W51 的是 09:04)。**规矩:凡是「真机上没生效」,第一件事先核 `dumpsys package … | grep lastUpdateTime` 与本地 APK 的时间**,再怀疑代码。(同类教训还有:`ws-close` 那次我以为「48 秒没重连」,其实是假后端踢掉连接本身就延迟了 19 秒,实际重连发生在 09:20:01。)
另:为它白跑了好几轮还有一个原因——**撤 `adb reverse` 不会掐断已建立的连接**,要真抖 WS 就用假后端的 `POST /__control/ws-close`。
### 4. 待办变化
- ✅ S2 结清(已实机验);S7① 结清(否定式实测 + 单测);**S7② 与 S3 未验**(见上)。
- 「REST 首屏失败、但 WS 从头到尾没断」这一格**仍是已知缺口**(没有任何状态变化可挂,项目纪律不许按时间轮)。
- S4/S5 仍需主人拍板;等后端两个面不变。