168 lines
12 KiB
Markdown
168 lines
12 KiB
Markdown
# 唤醒交接:机制能不能跟着换主会话?—— 实测结论
|
||||
|
|
|
|||
|
|
> 时间: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
|
|||
|
|
```
|