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