Files
dsh_ai1net_server/交付物/唤醒交接-机制能否跟着换主会话-20260930.md
T
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

169 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 唤醒交接:机制能不能跟着换主会话?—— 实测结论
> 时间:2026-09-30 18:03–18:25(本机)|会话:`6ecf6d98`(本棒)|新主会话:`67dfd365`
> 用户原话:「**可以试试,创建接续主会话后 能否在接续主会话上继续这个机制,旧的主会话停**」
> 产出:本文件 + `tmp/_resolve_probe.py`(只读探针)+ `tmp/_handover/waiter.json`(快照)+ `tmp/_handover/67dfd365-receipt.md`(新主会话自己的回执)
---
## 0. 一句话结论
**能,而且创建那一刻就自动跟着换了** —— 新建的「接续主会话」一出现,「主会话解析」立刻从旧目标切到它(依据=标题前缀 `主控`),**不需要任何额外动作**。
但有三条必须同时记住,否则「能解析」≠「能投达」:
1. 🔴 **只有"登记为 main 的那条不再出现在活会话里"才会换** —— 登记那条还开着 ⇒ 机制**钉死在它身上**,新主会话再好也不换(实测场景④⑤)。用户说的「旧的主会话停」正是这个**唯一触发条件**。
2. 🔴 **换过去 ≠ 投得进去** —— 新主会话此刻**不是桌面活动会话**,投递被拒(`no-live-match`)。⇒ **交接必须交给一条"正在跑 或 已被打开"的会话**,否则交接即断链(这正是历史上"静默断链"的成因)。
3. 🔴 **前缀判据与实际命名不一致**(`主控` ✅ / `[主控]` ❌)⇒ 本项目正在用的主会话标题**从来不是靠"显式前缀"被认出来的**,而是靠"工作区最近活动"兜底,任何一条本工作区会话活动更新就会静默顶掉它。
---
## 1. 机制里"主会话"是怎么定出来的
投递方 = `collabd.py --tick`(**宿主钩子唤起**,⛔ 不常驻)→ `_deliver_str()` → 找 `live == 目标` 的那个网关口 → `POST /api/v1/sessions/{id}/reply`。
`_deliver_str()` **不读死 id**,每次现算:
```
resolve_main() 三条路,按下面顺序短路:
① 登记(roles 里写着 main)**且它此刻在活会话集合里** → 认登记 source=roles:main
② 否则按工作区:cwd==本工作区 且 标题不含 [协作]
· 标题 strip() 后 startswith("主控") 的那条(显式标记,更可信) → source=prefix:主控
· 都没有 ⇒ 取"最近活动"的那条 → source=workspace
③ 还是解析不出 ⇒ 空串 ⇒ **调用方拒绝盲投**(⛔ 不投给任意会话)
```
换了(`switched_from` 非空)⇒ `_follow_main()` 改 `roles`(旧→worker/新→main)+ **显式告警**(⛔ 不静默)。
⚠️ 注意 ③ 的怪处:**活会话集合为空时(枚举失败/没口)反而"信任登记"**(`not live` 分支为真)⇒ 会解析出一条早关掉的会话。方向不算错(最终仍拒投),但**归因会误导**。
---
## 2. 本棒实测读数
| # | 场景 | 活会话集合 | 解析结果 | 依据 |
|---|---|---|---|---|
| ① | **真实(改动前 18:07)** | `[6ecf6d98]` | `6ecf6d98`(**本会话**) | `workspace` |
| ② | 同上,登记那条也不在 | `[6ecf6d98]` | `6ecf6d98` | `workspace` |
| ③ | 全无活会话(模拟) | `[]` | `fe146dd9`(**早关掉的那条**) | `roles:main` ⚠️ |
| ④ | **模拟:登记那条还活着** | `[...] + [fe146dd9]` | **`fe146dd9`** | `roles:main` 🔴 **被钉死** |
| ⑤ | 模拟:登记活着 + 新主会话也活着 | `[...] + [fe146dd9, 67dfd365]` | **`fe146dd9`** | `roles:main` 🔴 **被钉死** |
| ①' | **真实(改动后 18:12)** | `[6ecf6d98, 67dfd365]` | **`67dfd365`** ✅ | `prefix:主控` |
关键对比:**④⑤ vs ①'** —— 新主会话有没有被认,只取决于「登记那条在不在活会话里」,
与「另一条老会话(`6ecf6d98`)还开着」**无关**(因为它不是登记的那条)。
### 交接实验(真做,非模拟)
| 步骤 | 结果 |
|---|---|
| 建一条一次性自动化(`42be9561`,18:11 触发,名=`主控 · 机制线 · 接续主会话(唤醒交接验证)`) | ✅ 已建(属「接续会话」类,白名单内免确认) |
| 它起出来的会话 | ✅ `67dfd365`,标题=自动化名**逐字**,cwd=本工作区,`18:12` 跑完(`completed`) |
| 新会话自己写回执 | ✅ `tmp/_handover/67dfd365-receipt.md` —— **"解析出来的就是我,依据 prefix:主控"**,独立复核与本节一致 |
| 新会话自己带的网关口 | ✅ 运行期间 `127.0.0.1:64766`,`live=67dfd365` —— 跑完口即消失 |
| 现在还能投进去吗 | ❌ `no-live-match`(它已不在任何网关口的 `live=` 上) |
---
## 3. 两个真缺陷 + 一个空窗期现象
### 🔴 缺陷 A:前缀判据不认**带方括号**的写法(代码与命名规范打架)
代码认 `title.strip().startswith("主控")`,而**本项目自己在用的主会话标题是 `[主控]-[机制线]-接续:…`**(带方括号)⇒ 判 **False**。
后果:这条主会话**从来不是**靠 `prefix:主控` 被认出来的,而是靠 `workspace`("最近活动")兜底。
⇒ 本工作区里**任何一条会话只要活动更新,就会静默把它顶掉**(无告警、无症状,只表现成"通知投到别的窗口去了")。
### 🔴 缺陷 B:**登记(路径依赖)压过显式前缀**(="旧的不停就不换")
`① 登记且还活着 ⇒ 认登记` 这条短路**排在** `② prefix:主控` **前面**。
⇒ 只要旧登记那条还开着,用户**新建的主控会话永远接管不了**(实测④⑤)。
⇒ 这条**恰好是用户要的语义**("旧的主会话停"),但它意味着:**"显式标记 > 路径依赖"这条正确优先级目前是反的。**
### ⚠️ 空窗期现象:旧的停了、新的没开 ⇒ 投递被**拒**
实测:新主会话不在活会话集合里 ⇒ 投递路径在 `resolve_main` 之后**找不到匹配的网关口** ⇒
走 `main-not-live` **拒投 + 告警**(⛔ 不盲投、⛔ 不静默)—— **这是设计正确的行为**,但它意味着
**"交接"必须交给一个正在跑 / 已被打开的会话**,否则交接当天就断链。
### ✅ 已排除的两个"疑似缺陷"
- `discover_gateways()` 只看到 2 个网关、`wake-session.py --list` 看到 3 个 —— **不是缺陷**:前者按 `cwd == 本工作区` 过滤(已归一化斜杠/大小写),后者列全部回环口。
- 场景 ③"全无活会话 ⇒ 认登记" —— 有误导性但**不导致误投**(后续仍拒投)。
---
## 4. 正确的交接姿势(操作序列)
1. 新主会话**标题以 `主控` 开头**(⛔ 别写 `[主控]`,见缺陷 A)。
2. **先把旧主会话从桌面上切走/关掉**(⛔ 不是删会话、⛔ 不是杀进程 —— 只要它不再出现在任何网关口的 `live=` 上;实测④⑤ 证明只要它还 live,机制就一直钉在它身上)。
3. **同时把新主会话在桌面上打开**(它必须在某个网关口的 `live=` 上,才投得进去)。
4. 以上三步做完,下一次投递就会:解析→新会话 ⇒ `_follow_main` 改 `roles` ⇒ 投达 + 显式告警。
⛔ **不要做的**:把两条主会话**同时**开着(会被钉回旧的那条);用带方括号的标题(会静默降级为"最近活动")。
---
## 5. 泳道图:机制换主会话,卡在哪一格
```
触发源 投递方(collabd --tick) 解析器 resolve_main 目标会话
(钩子) (宿主进程树内,带口令) (三条路短路) (要收通知的那条)
──────────────────────────────────────────────────────────────────────────────────────────
钩子事件 ──▶ 有内容要投 ──▶ ①登记那条在活会话里?──是──▶ 用登记 ═══════════▶ 投给【旧的】
│ 🔴【红】这格把新主会话挡在外面(缺陷B)
└─否─▶ ②有标题以 主控 开头?─有─▶ 用那条 ═══════▶ 投给【新的】 ✅
│ 🔴【红】[主控]-… 判 False(缺陷A)⇒ 落到③
└─否─▶ ③本工作区最近活动 ────────────▶ 随便一条(谁新是谁)
⚠️【第二红】投达前还有一道:目标必须出现在某网关口的 live= 上
新的没在桌面打开 ⇒ ❌ 此处断(拒投 + 告警,不静默)
```
---
## 6. 事故链:历史上"静默断链"是怎么长出来的
```
主会话标题写成 [主控]-… (命名规范与代码判据不一致 → 缺陷A)
↓ 缺防线①:没有"前缀写法兼容"的单测/自检
真正的主会话只能靠"最近活动"兜底
↓ 缺防线②:没有任何"当前主会话 vs 登记"的交叉校验
别的会话活动更晚 ⇒ 通知静默投到别的窗口
↓ 缺防线③:投递成功只看 HTTP 200,不回查"消费方真的动了吗"
主会话以为"没人在跑" / 协作方以为"已通知" ⇒ 双方各说各话
↓
登记那条被关掉后,机制不换(缺陷B + 只有"登记失效"才换)
↓ 缺防线④:换了没有显式告警(早期版本)
断链 → 排查从"为什么没唤醒"开始,实际是"从来没指向对过"
```
**本该拦住的三道防线**:① 前缀判据兼容实际命名(含方括号)+ 自带单测 ② **每次投递都做"登记 vs 显式前缀"交叉校验**,不一致就换+告警 ③ 投递后回查目标会话是否真跑了一轮。
---
## 7. 待拍板(一轮一问)
**问题**:交接已经自动生效(机制现在指向新起的「接续主会话」),但它现在处于"没在桌面打开"的状态 ⇒ 投递会被拒。要不要把它**真正接通**,以及要不要顺手修掉那个"标题写法不兼容"的判据?
- **A. 你在桌面上打开那条新会话**(我这边零动作)—— 优点:一步到位、零风险、零改动;缺点:必须你动手一次,且之后旧窗口不能再同时开着。
- **B. 我改掉那个"只认不带方括号写法"的判据**(顺带把"显式前缀"提到"登记"前面)—— 优点:以后交接不再挑标题写法、也不再被旧窗口钉住;缺点:这是**跨工作区共用的机制**,改它要拿独占锁、且不能边改边用,得单独开一棒。
- **C. 维持现状** —— 优点:什么都不动;缺点:机制指向的是一条投不到的目标,下次投递会报"主会话不在活会话里"。
**我的默认(无人值守下已按此执行)**:**只做验证,⛔ 不动共用机制代码**(B 属机制层,须独占锁、单独一棒);新会话与自动化**保留**(它是你要的交接产物)。回退极简单:把 `67dfd365` 的标题改掉、不再以 `主控` 开头,解析就会退回原样。
---
## 8. 出处与可复现命令
```
# 只读探针(三条路 + 工作区候选 + 模拟"旧会话是否还活着")
"E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" tmp/_resolve_probe.py
"E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" tmp/_resolve_probe2.py
# 现查网关与各自 live(⛔ 端口每次都变,别记端口)
"E:/ProgramData/.workbuddy/binaries/python/versions/3.13.12/python.exe" .workbuddy/collab/wake-session.py --list
# 解析逻辑原文
E:/ProgramData/.workbuddy/skills/multi-session-collab/scripts/collabd.py (resolve_main §1545 / _follow_main §1598 / _deliver_str §938)
# 新主会话自己的回执
tmp/_handover/67dfd365-receipt.md
```