- 变更规模:新增 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/ 知识文件,按口径入库)
12 KiB
唤醒交接:机制能不能跟着换主会话?—— 实测结论
时间:2026-09-30 18:03–18:25(本机)|会话:
6ecf6d98(本棒)|新主会话:67dfd365用户原话:「可以试试,创建接续主会话后 能否在接续主会话上继续这个机制,旧的主会话停」 产出:本文件 +tmp/_resolve_probe.py(只读探针)+tmp/_handover/waiter.json(快照)+tmp/_handover/67dfd365-receipt.md(新主会话自己的回执)
0. 一句话结论
能,而且创建那一刻就自动跟着换了 —— 新建的「接续主会话」一出现,「主会话解析」立刻从旧目标切到它(依据=标题前缀 主控),不需要任何额外动作。
但有三条必须同时记住,否则「能解析」≠「能投达」:
- 🔴 只有"登记为 main 的那条不再出现在活会话里"才会换 —— 登记那条还开着 ⇒ 机制钉死在它身上,新主会话再好也不换(实测场景④⑤)。用户说的「旧的主会话停」正是这个唯一触发条件。
- 🔴 换过去 ≠ 投得进去 —— 新主会话此刻不是桌面活动会话,投递被拒(
no-live-match)。⇒ 交接必须交给一条"正在跑 或 已被打开"的会话,否则交接即断链(这正是历史上"静默断链"的成因)。 - 🔴 前缀判据与实际命名不一致(
主控✅ /[主控]❌)⇒ 本项目正在用的主会话标题从来不是靠"显式前缀"被认出来的,而是靠"工作区最近活动"兜底,任何一条本工作区会话活动更新就会静默顶掉它。
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. 正确的交接姿势(操作序列)
- 新主会话标题以
主控开头(⛔ 别写[主控],见缺陷 A)。 - 先把旧主会话从桌面上切走/关掉(⛔ 不是删会话、⛔ 不是杀进程 —— 只要它不再出现在任何网关口的
live=上;实测④⑤ 证明只要它还 live,机制就一直钉在它身上)。 - 同时把新主会话在桌面上打开(它必须在某个网关口的
live=上,才投得进去)。 - 以上三步做完,下一次投递就会:解析→新会话 ⇒
_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