Files
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

329 lines
101 KiB
Markdown
Raw Permalink 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.
---
name: session-mechanism
description: 「**会话机制 + 多会话执行**」的合并总入口(原 `multi-session-collab` + `workbuddy-session-forensics` 已并入本包)。🔴 **2026-10-02 起会话只两类:主会话 + 执行会话**(唤醒会话/跟进会话/队列上报**已整套退役**,见文首口径块)|🔴 **2026-10-03 起「上报」整套真删**——现行机制=**协作程序接收(`--report` 写台账)+ 处理(建检查会话排期)**,⛔ **队列变化不再自动通知任何人**,要落事得**显式建执行会话**。两段可分别加载:① **会话机制** —— 管「会话怎么活下去、怎么不哑掉、怎么不失忆、跨机器怎么一键装好」:治「会话卡住 / 一直转圈 / 发消息没反应 / 界面不刷新」「会话日志涨到上限把界面顶死」「上下文爆了要换会话 / 接续会话怎么开」「抢锁 / 执行锁 / 并发 / 别的会话在动」「钩子没生效 / 钩子把我也拦住了」「换一台电脑要重装这一堆钩子」「某个历史会话当时到底干了啥」。② **多会话执行** —— 管「一个主会话带多个执行会话把需求做完」:治「多个会话协同但工期被拖长 / 有会话在干等 / 链条断了没人接 / 分不清『真完成』还是『只排了下一棒』/ 问清主会话·执行会话两类怎么分(**接续会话是撞阈值时的「形态」,⛔ 不是第 3 类**)」。🔴 **主触发句(2026-10-03 用户定案)**:用户说「**使用执行会话完成 XXXX 目标**」「**继续 XXXX 目标**」时**直接走本技能**(=要派执行会话去把一个目标做完/接着做)。其他触发:「怎么协同多个会话」「别的会话都在干等」「任务没推进」「链条断了」「你监督这些会话」「会话卡住」「日志要爆了」「换机器怎么配」「升级 / 安装这套机制」。核心=**会话机制三件套(钩子 + 锁 + 日志闸)全局生效** + **执行两条通道各走各的**(派活靠自动化/收结果直读宿主库/机械判定下沉到常驻程序/人只看一个看板)+ **一键配置 `install.py`**(换机器不手抄绝对路径)。
version: 1.2.0
updated_at: 2026-10-03
last_change: 2026-10-03 16:3x · 🔴🔴 **用户定案:各工作区的常驻由「它自己的主会话」起 + 看板只保留一份**(本条为准,⛔ 排在 16:2x 那条之上):① **用户口径(逐字)**「**每个工作区 会话协作机制的主会话自己创建后台任务 启动常驻协作程序**」+「**后台看板不用运行这么多 共享一份就可以**」。② **落地**:两条主会话排期(`[主]-会话协作测试1/2-主会话`)由 `once` 改 **`recurring`(`FREQ=HOURLY;INTERVAL=1`)**,并把**开工第 0 步**改成「确认本工作区协作程序在跑(判据 `pid` 活 ∧ 心跳距今 < 90 秒),不在 ⇒ 用后台任务起一次、输出重定向到日志」(⛔ 措辞按 P0-20 中性化:⛔ 不出现"常驻/守护/自维持"、⛔ 不新建周期排期、⛔ 不循环等待)。③ **看板共享**:两新区配置里**没有** `board_port`(只有各自 `singleton_port` 20540/23992,本区 20099)⇒ 只保留本区 `8788` 这一份,靠 `peer_workspaces` 并列查看其它区。④ 🔴🔴 **最关键的实测(决定这个口径能撑多久)**:**后台任务的寿命 ≈ 发起它的那个会话的寿命** —— **一次性会话**起的那份只活 **5 分钟**(14:42:00 起 → 14:47:10 最后心跳,`round=32`),**长期存在的会话**起的那份已连续 **4.5 小时**(12:03 起仍在跑,`round=1624`)⇒ 所以主会话**必须改成周期性**才能反复补,⛔ 但仍**不等于长期在线**。⑤ **验证(现算)**:三区常驻**同时在跑**(本区 pid 36868 起于 12:03 / 测试1 pid 6148 / 测试2 pid 26736 均起于 16:35:53),共享看板上两新区的 `heartbeat_age_min` 都是 **0.0 分钟前** ⇒ 一份看板看得到三个区各自在线。⑥ ⚠️ **如实报**:两新区的常驻是我**临时补起**的(本会话结束即停),长期靠各自主会话每小时补一次 ⇒ 要**真正长期在线**,还差一层**脱离会话**的载体,**待定**。|**前情** 2026-10-03 16:2x · 🔴🔴 **跨工作区 tab「只换标题、不换数据源」⇒ 看板在说假话(已修)+ 新坑 P0-40**(本条为准,⛔ 排在 16:0x 那条之上):① **用户报障**「**选择另一个工作区目标 tab 下面没有显示对应工作区目标和执行情况**」⇒ **我第一反应猜错方向**(以为"没显示")⇒ 实测是**显示了假的**:三格 `labor`/`sessions`/`progress` **md5 完全相同**(`build()` 的 `tasks`/`srows`/`st` **只取一次、所有格共用**,只有 `goal` 跟着格换)⇒ 把**本工作区**的执行情况挂到了「会话协作测试1/2」名下。② **修法三条**:**每格数据源跟着格走**(peer 格只喂 `_peer_tasks()`/`_peer_srows()`/`_peer_state()`,只读对方目录,读不到 ⇒ 空 + 界面 `renderPeerNote()` 如实说明,⛔ 绝不拿本区补位)+ **单独补捞**(`_session_rows()` 只取**全库最近 50 条** ⇒ 对方会话一条都不在里面 ⇒ 新增 `_peer_session_rows()` 按 `cwd` 精确查;⛔ **不许**放大全局 `limit` —— 那会让**本区** `others_running` 计数暴涨)+ **作用域换到对方**(`_sessions()` 有 `_ct == _wstail` 的 `cwd` 硬过滤,而 `_wstail` 取自全局 `WS` ⇒ peer 格把 `sc["workspace"]` 覆盖成对方根)。③ **验收(全部现算)**:本区 tasks **323 字节** / peer tasks **2 字节**(= `{}`)⇒ 两侧不再同源;新用例 `t_peer_block_no_self_data`(5 项)+ **变异对照**(把 `_peer_tasks(_wr)` 换回本区的 `tasks` ⇒ ①⑤ **双双报红**、⑤ 复现假数据本体「peer tasks=1 件 / 两者相同 True」;还原后复绿);自检 **PASS 62 / FAIL 0**;前端两块 JS `node --check` 全过;`--serve 8788 --takeover` 重起后线上复核通过。④ 🆕 **P0-40**:凡做「**一格一视图**」(tab/分栏/多租户面板)必先答一句「**这一格的数据源是不是跟着格走**」,且**验收必须逐格比对关键字段的 md5**(这次就是靠 md5 抓出来的);⛔ **"没显示"与"显示了错的"必须先分清再动手**。⑤ ⚠️ **如实报未解决**:peer 格 `sessions` **仍为空**(卡在 `in_project()` —— 主会话登记与任务类别都在**对方** INBOX,而 `project_scope()` 读的是本工作区;⛔ 我没去改那条,它与 `collabd.py::_in_project()` 有**逐条同款**硬约束,历史已踩三次)⇒ 正解在**架构层**:**各工作区开自己的看板**(各自进程、各自判据),跨区只做**总览 + 跳转**。|**前情** 2026-10-03 16:0x · 🔴🔴 **看板 tab 跨工作区并列查看 + 新坑 P0-39**(本条为准,⛔ 排在 07:2x 那条之上):① **用户报障**「**为什么 会话协作看板 tab 选项不能切换看另外两个工作区的目标**」⇒ 真因=`board.py::goal_files()` 只扫 `INBOX/goal.json` + `INBOX/goals/*.json`,而 `INBOX` 由**部署配置的 `workspace`** 决定 ⇒ **一个 `--serve` 实例天生只看见一个工作区**(⛔ 不是前端 bug、⛔ 不是页面没刷新)。② **修法**:配置新增 `peer_workspaces`(**正斜杠**路径列表)⇒ 把对方 `goal.json` 读进来当**额外一格 tab**、打 `peer` 标记;🔴 **严格只读**(⛔ 不写对方文件/⛔ 不起对方进程/⛔ 不改对方状态,活跃目标仍只有本工作区那份 ⇒ `collabd.py` 零改动);⛔ 排除本工作区(否则活跃目标出现两格)+ 去重键带**工作区前缀**(否则同名目标互相顶掉)。③ **改了四处**:`board.py`(新增 `_peer_workspaces()`/`goal_files()` 收 peer/`_goal_block()` 加 `peer`·`ws_root` 形参并透传 —— ⛔ 形参漏写 ⇒ 调用点 10 实参 vs 8 形参 ⇒ `TypeError` ⇒ 快照生不出 ⇒ 看板一直 `warming`)|`assets/board.html`(tab 显示 `g.peer` 标记)|`selftest.py`(新用例 `t_tab_peer_workspace`,11 项)|生产 `collabd.config.json`。④ 🔴 **新坑 P0-39**(本轮自己栽的,同族第 2 次):新用例的「现网真读数」项报 `goal_files()=1 格`,而**同一时刻命令行直接加载是 3 格** ⇒ 真因=`imp()` 把 `COLLABD_CONFIG` 指到 `tmp/selftest/`,而 board 的 `C = _cfg()` 是**模块级**求值 ⇒ 用例内永远只看见测试那 1 格 ⇒ **判据钉在夹具上,恒绿与恒红都是假象**。正解=**加载前换 env、用完 `finally` 还原** + **真磁盘上造目标** + **变异对照**(把 `for _pw in _peer_workspaces()` 打掉 ⇒ ⑧⑩ 双双报红;还原后 md5 校回原值 ⇒ `PASS 61 / FAIL 0`)。⑤ **重启验收**(改 `board.py` 必须重启,P0-38):`--serve 8788 --takeover`(停旧 pid 34660)⇒ 直读 `/board.json`:`goals`=**3 格**、`peer`=`会话协作测试1`/`会话协作测试2`(⛔ 区名是历史名,不改)、`active`=**1**。⑥ **顺带订正**:§2 首条「四类会话」**已标注作废**(现行两类,见文首口径块);临时探针 7 份已清。|**前情** 2026-10-03 07:2x · 🔴🔴 **看板两处几何改动 + 新增 P0-38「改看板要不要重启有四类答案」**(本条为准,⛔ 排在 07:0x 那条之上):① **架构图 ④ 两格合并成一格** —— 我第一版把「上报」改名成「常驻」,用户逐字驳回「**怎么又把上报 改成常驻了 常驻什么,不是 协作程序常驻吗**」⇒ **根因=把「角色名」与「运行形态」混成一个词**(角色本来就叫**协作程序**,"常驻"只是它的一种跑法)。✅ 处置=`PX=280, PW=720` 一格,标题「协作程序」,副标题「接收:--report 写台账 · 处理:建检查会话排期 · 常驻一直运行」,右侧小字「⛔ 队列投递已于 2026-10-03 退役(曾叫「上报」)」;删 `UX`、删随之悬空的 `RKX`、`TICKX` 改算 `PX+PW/2+120`;⚠️ 备份 `board.html.bak-两格合一-20261003-0720`;自写检查器 `tmp/_check_board_js.js` 确认 13 个已删变量在**代码行**零引用。② 删「最近上报 `wakeups.jsonl`」整张卡(实测文件根本不存在 ⇒ 恒空假面板)。③ 🔴 **新坑 P0-38**(源于用户第二句纠正「**看板每次修改都不处理看板**」)—— 我把"改配置要重启"说成"改看板都要重启",**分类错误**;现跑实证(`tmp/_probe_hotreload.py`,改 `<title>` ⛔ 未重启 ⇒ 页面立即拿到)坐实四类:**改 `board.html`/`board_ext.py` ⛔ 不用**(`board.py:1293` 每请求 `read_bytes()` / `EXT_CACHE` 按 `(mtime,size)` 热重载)|**改 `board.py`/`collabd.config.json` ✅ 必须**(`C = _cfg()` 在 `board.py:81` 模块级执行一次)⇒ **判据=先答"这个文件是谁在读、什么时候读"**,⛔ **重启不是万能药**。④ 验收:两 script 块 `new Function()` 全过 | `selftest` **PASS 47 / FAIL 1**(与合并前同基线)| 看板 pid 43220/HTTP 200 | 截图 `tmp/board_merged.png`。|**前情** 2026-10-03 07:0x · 「上报」整套**真删**(用户原话「**没用了就删除,现在的机制是 协作程序接收和处理队列**」):`supervise()` 203→71 行(-132,删①′待反馈序列+②③单条握手+④唤醒+两个 `_deliver_str()` 调用点)、删 `--tick` 里 `check_delivery_consumed()` 调用、`wake_enable` true→false;**它是三重死锁不是一条腿断**(投递失败⇒不消费⇒队首永驻死件 `S8=done`⇒`no_fb` 恒 False⇒唤醒条件②永不成立);删前实测 `follow-retired` **1 313 行且每 20 s +1**、`wakeups.jsonl` 文件不存在;⛔ 保留 4 个零调用点函数(`selftest` 有 4 处**真调用**前两个,删函数体=`NameError`)⇒ ⚠️ 判"函数体内没有调用 X"**必须走 AST**(`selftest.py::_calls_in()`),字面 grep 会被退役 docstring **永久假红**;本轮新坑 **P0-35**(每轮 `unlink` ⇒ 撞宿主 SafeDelete 批量删除护栏 ⇒ 常驻被系统终止,与 P0-6 同根因第二次复发 ⇒ 修法=**一辈子只删一次**的状态位)/**P0-36**(AST 判据)/**P0-37**(**角色退役 ≠ 承载它的进程退役** —— `--supervise` 的"建检查会话排期"职责仍只有它承载 ⇒ 留进程改名,⛔ 不删节点)。|**再前情** 2026-10-02 15:3x · 🔴🔴 **钩子超时是"拦截"不是"变慢"**:用户报「`UserPromptSubmit operation blocked by hook: Hook timed out after 20000ms`」。① **真因(实测,⛔ 非推断)**:`wb-result-hook.py` 在 `UserPromptSubmit`(注册 20 s)上串跑 `collabd --gap` **13.5 s** + `--once` 0.5 s + `--tick` **13.5 s** ≈ **27.5 s** ⇒ 宿主掐断 ⇒ **用户那句话没被提交进去**。② **修法四条**(详见 §1 新增「钩子总预算」):开局认领预算(`18/25/8`)+ 门槛按**实测**给 + 不需要结果的活**后台**跑 + **一轮一个贵活** 且慢产物**落缓存**。③ ✅ **实证**:钩子 **27 s → 0.5~1.9 s**(三次冒烟,注入内容照旧含「缺会话 ⇒ 自动拉起」);后台 `--gap`/`--tick` 在父进程退出后**仍跑完**(15 s / 11 KB)。④ **副产品**:`SessionEnd` 只给 10 s 却挂着 13.5 s 的 `--tick` ⇒ **它从来没跑完过**;⚠️ 本机 `CREATE_BREAKAWAY_FROM_JOB` **必失败**(`PermissionError 13`)⇒ 别加。⑤ 备份 `wb-result-hook.py.bak-hooktimeout-20261002-1520.py`。|**前情(2026-10-03 07:0x)** 🔴🔴 **常驻起法实测订正** —— ① 用户连续两次纠正「谁告诉你的后台进程活不过几十秒,那协作看板如何开启的 …那个工作台是如何一直运行的」「怎么启动进程 技能中都没有记录吗」⇒ 我此前把「**从工具调用进程树里起的活不过当轮**」误推成「**本机不存在长跑进程**」,并据此建议「改用排期当时钟」。① **实测坐实**:同一时刻 `start /b` 与 `Popen+DETACHED` 各起一条每秒打点的探针 ⇒ 两条**活到约 11 分钟后停在同一 tick**(67/64 行)⇒ **差别不在起法关键字,在父链**。② **反证**:MCN 工作台 `mcn-work-shop/start.bat`(`start "" node.exe server.js 8900`)**用户从桌面双击**、属用户登录会话 ⇒ **一直活着**;另有 4 个 `node.exe`(`会话名=Console`)长期存活。③ ✅ **正解=WorkBuddy 自己的后台任务**(工具 `run_in_background`;⛔ 别用 `subprocess` 自己造)—— 本轮现算两条:`board.py --serve 8788 --takeover` ⇒ **LISTENING pid 43176/HTTP 200/119 704 B**;`server.js 8900` ⇒ **LISTENING pid 14056/HTTP 200/1 797 B**。⚠️ 一次带 `--takeover`(静默并存会看到旧图)|⚠️ 子进程用 `pythonw.exe`(GUI 子系统 ⇒ Windows 永不分配控制台 ⇒ ⛔ 不闪窗)|⚠️ `pythonw` 无 stdout ⇒ **必须显式重定向到文件**(`tmp/board-serve.log`),否则静默无痕连"起没起"都查不到。④ 🔴 **启完必查三样**(⛔ 打印"已启动"≠ 成了):`netstat` 有 **LISTENING**(⛔ `TIME_WAIT`/`FIN_WAIT_2` 是历史连接残留,不算)+ `curl` 得 **200** + `tasklist` 进程在。⑤ ⛔ **撤回**「改用排期当时钟」那条建议:唤醒时钟**回到常驻进程本身**,排期仍保留但身份是复活兜底。⑥ ✅ **S8 自愈仍保留**(`--supervise` 心跳 + `--tick` 顺手续命 + 判据 `pid 活 ∧ 心跳新鲜(<90 s)`),⛔ **理由已换**:⛔ 不是"没法长跑",而是**常驻总会被打断**(重启/换会话/手动收)。⑦ ✅ **贯通四处**:`SKILL.md §2` | `architecture.md`(当前结论表「本机起法」行 + §5-1 代价②整段)| `scripts/collabd.py` 用法头| **`last_change` 本条**(⛔ 否则别人第一眼看到的还是旧结论)。|**前情** 2026-10-02 15:3x · 🔴🔴 **钩子超时是"拦截"不是"变慢"**:用户报「`UserPromptSubmit operation blocked by hook: Hook timed out after 20000ms`」。① **真因(实测,⛔ 非推断)**:`wb-result-hook.py` 在 `UserPromptSubmit`(注册 20 s)上串跑 `collabd --gap` **13.5 s** + `--once` 0.5 s + `--tick` **13.5 s** ≈ **27.5 s** ⇒ 宿主掐断 ⇒ **用户那句话没被提交进去**。② **修法四条**(详见 §1 新增「钩子总预算」):开局认领预算(`18/25/8`)+ 门槛按**实测**给 + 不需要结果的活**后台**跑 + **一轮一个贵活** 且慢产物**落缓存**。③ ✅ **实证**:钩子 **27 s → 0.5~1.9 s**(三次冒烟,注入内容照旧含「缺会话 ⇒ 自动拉起」);后台 `--gap`/`--tick` 在父进程退出后**仍跑完**(15 s / 11 KB)。④ **副产品**:`SessionEnd` 只给 10 s 却挂着 13.5 s 的 `--tick` ⇒ **它从来没跑完过**;⚠️ 本机 `CREATE_BREAKAWAY_FROM_JOB` **必失败**(`PermissionError 13`)⇒ 别加。⑤ 备份 `wb-result-hook.py.bak-hooktimeout-20261002-1520.py`。|**前情** 2026-10-02 08:1x · 🔴🔴 **S8 治本:常驻的载体(「跨 turn 存活」落地)**。① **症状**:常驻 `--supervise` 历次只活 **8 / 12 / 20 分钟**,而日志照旧在走 ⇒ **四棒**据此误判"常驻在跑"(那些轮次其实是**宿主钩子**的 `--tick` 写的)。② **实测两条(⛔ 非推断)**:`CREATE_BREAKAWAY_FROM_JOB` **被宿主作业对象拒绝**(`PermissionError(13,'拒绝访问。')`)⇒ 脱不出回收;而普通子进程**能**活过**工具调用边界**(三探针跨调用打点 40 s+、父进程早已消失)但**迟早被回收** ⇒ **本机不存在"一直活着"的进程**。③ **修法=事件驱动的常驻自愈**:`--supervise` 每轮写**心跳**(`logs/supervise-heartbeat.json`,原子替换/**零删除**)+ 节拍(`…-heartbeat.log`,超 512 KB **重写**)+ 启动时**单例让位**;**`--tick` 顺手续命**(`ensure_supervise()`:幂等 · 30 s 节流 · **无口令不起** · 无配置不起)+ 新增 `--ensure`。④ 🔴 **存活唯一机读判据**=`pid 活 ∧ 心跳新鲜(<90 s)` ⇒ 此前**无人写心跳**、`session-rules-check` 第 ⑩ 项**恒 warn**,现已可绿。⑤ **实证(现算)**:07:55:08 杀掉 `pid 48248`(`AFTER_COUNT=0`)⇒ 07:55:17 一次 `--tick` ⇒ 07:55:18 新实例 `pid 47156`(`round=1`);`_collabd.log` 三次续命记录**全是 `why=tick`**。⑥ **同轮两个自坑已修**:`_pid_alive` 第一版走 `tasklist`(输出 **GBK** ⇒ `text=True` 抛 `UnicodeDecodeError` ⇒ 判据**静默变假**)⇒ 改内核句柄(`OpenProcess`+`STILL_ACTIVE`,`ACCESS_DENIED` **保守判"在"**);自测的 `--tick` 会在**测试工作区**起真常驻 ⇒ 夹具被持续重写 ⇒ `已停总闸` 用例**假红**、FAIL 数每轮不同 ⇒ `_mk_env()` 显式 `COLLABD_NO_ENSURE=1`。⑦ **贯通**:`collabd.py`(用法头/`--ensure`/`ensure_supervise`/心跳)+ `architecture.md`(当前结论表「本机起法」与「投递」两行重写 + §5-1 起法整段重写 + §8 落地映射 + §9 新条)+ `pitfalls.md` **P0-22** + `selftest.py` 新用例 `t_supervise_ensure`(8 项)。⑧ ✅ **回归**:`selftest` **PASS 46 / FAIL 0**;`py_compile` 全过。⚠️ **残留(如实报)**:本机**不存在**"跨全静默期仍活着"的进程 ⇒ 引擎全无事件时最长空窗 = 下一条 `[唤醒]` 排期的间隔(≤1 h,身份=**复活兜底**,⛔ 不是时钟)。|**前情** 2026-10-02 07:0x · 🔴🔴 **口径纠正(用户逐字):「不是规划 是 规则」** —— 把「开工第 0 步」的对象由**会话规划**改成**会话规则机制**。① 🔴 **为什么要改名**:首版只查**排期**那一面,而当天实测出的三类失效(钩子注入指向**已退役技能名** / 常驻快照**写进幽灵目录** / **每轮注入的记忆**指针悬空)**一条都不在它的检查范围内** ⇒ 名字还叫「规划」就成了第二次「在册 ≠ 生效」。② ✅ **收敛成一个入口**:新脚本 `scripts/session-rules-check.py` **查三类十二项**(A 机制装没装好 ①②③④ | B 规则载体同没同步 ⑤⑥ | C 编排在不在跑 ⑦⑧⑨⑩⑪⑫),状态标记 `<WS>/.workbuddy/collab/session-rules.json`;旧 `session-plan-check.py` **退役**到 `<WS>/归档/技能包-旧件-20261002/`(内含《为什么退役》),**能力不回退**(原六项逐项并入 ⑦⑧⑨⑩⑪⑫)。③ 🔴 **`cwds` 判据收紧(当场被实跑打回的两条假红)**:原判据「同父目录 + 字面不同」在 `AIProject/` 这种**多业务线平级目录**下把**别人那条线**全报成失配(实测 `aigc-idea-impression`/`ai1net-decision-laya`)⇒ 改成「同父目录 **+ 名字去 `-`/`_` 后仍相同**」**或**「同名不同父目录」。④ ✅ **判据自己也被验了**:⑨⑫ 两项抽成**纯函数**(只吃数据、便于对照)+ 合成样本夹具 `<WS>/tmp/rules-check-mutate.py`:**原版 PASS 10 / FAIL 0**,**四个变异体**(判据恒空/放宽成"同父即报"/把"没跑过"当"跑完了"/不排除"还有下次触发")**逐一按预期报红**;⚠️ **夹具本身先红两次**(期望值写死:漏算一条样本 + 用**有序列表**比中文名)—— 又一次坐实「**断言里写死的期望值 ⇒ 迟早假红**」。⑤ ✅ **贯通四处**:`state.py §5b`(脚本名/标签 `[会话规则]`/免锁例外条款里的产物名)+ 本节 §2 + 本 frontmatter + `references/manifest.md`。|**前情(⚠️ 用词已被上一条纠正:规划 ⇒ 规则机制;其余记录照原字保留)** 2026-10-02 06:3x · 🔴🔴 **新增「开工第 0 步 = 会话规划体检」**(用户 2026-10-02 明令,原话逐字:「**这个会话和协作会话的技能包 运行的第一件事 ,就应该是检查清楚 所有会话规划是否配置完整且生效,然后标记一个状态**」)⇒ 新增 `scripts/session-plan-check.py`(查六项:**周期钟 / 模型可用性 / cwds 同形 / 活会话 / 投递心跳 / 从未运行的一次性排期**),结论**落成状态标记** `<WS>/.workbuddy/collab/session-plan.json`;**接进工作区 `state.py`**(跑状态快照即自带这一段,⛔ 不必另记一条命令)+ `SKILL.md §2` 立为**开工第 0 步**(**排在「建会话」之前:先查清、再补建**)。🔴 **首版判据两处想当然、当场被实测打回**(都是"判据看着在、其实不成立"同族):① 要求「**协作**」类也有周期钟 ⇒ **假警报** —— 协作会话是**按需创建**的(用户 2026-10-01 口径「随需求确定时创建」)⇒ 改为**只要求唤醒 / 跟进**两侧的钟;② 用 `automations.last_run_at` 判「排期跑没跑过」⇒ **该字段宿主根本不写**(实测:本会话自己那条 `接续 · 会话机制合并包 · 任务4b-4d-6` 明明在跑、值仍是 `None`)⇒ 把 **18 条正常痕迹全报成故障** ⇒ 改用 `automation_runs` 表判"运行过"。✅ **变异对照**(把 `--ws` 指到别处)⇒ 周期钟 / cwds 两条**按预期报红** ⇒ 证明判据非恒绿。|**前情** 2026-10-02 06:0x · 🔴 **术语统一(用户「语言全部统一」)** —— 全包替换 **145 处 / 17 个文件**:`心跳`、`唤醒脉冲`、`唤醒轮` ⇒ **唤醒**;`监督程序` ⇒ **投递**;例外两处(`心跳时钟`⇒`唤醒时钟`、`心跳源`⇒`唤醒源`)。⛔ **「」内的引用原话、以及"讲旧名/讲历史"的 39 行一律保留原字**(改了就是篡改证据)。✅ 回归 `selftest` **PASS 45 / FAIL 0**、`install.py --verify` **全绿**、`--manifest` **34 份 / 语法失败 0**、看板线上页与本地 **md5 一致**(读文件渲染 ⇒ 无需重起)。|**前情** 2026-10-02 05:4x · 🔴🔴 **三件事 + 一次自我纠错**。① 🔴🔴 **纠错(最重要)**:上一条把 `automation-request-refused` 归因成「prompt 措辞触发内容审查」—— **方向错了**。真因(会话日志 `details` 逐字)=**排期绑的模型不支持关闭思考**:`model_id=deepseek-v4.1-flash` + `model_is_thinking=0` ⇒ 服务端 `-32603`;对照组 `hy4-preview` + `is_thinking=1`(日志清理/决策线体检)**从未被拒**。⇒ **判据改**:遇到该码**第一步就读那条会话自己的日志 `details`**,⛔ 不从错误名 `refusal` 推断成因("概率性"来自宿主的 `thought-level-fallback` 分支,与文本无关)。② ✅ **治本**:两条周期排期思考档打开(`automation_update` 传 **`modelIsThinking=true`** —— ⚠️ 该字段**不在工具的文档 schema 里**但 `additionalProperties` 接受、**实测生效**;**回读宿主库 `model_is_thinking` 0→1 才算数**)。③ ✅ **孤儿全局执行锁已解除**(用户 10-02 明令):OWNER `会话协作-机制排查与修复-20261001`(10-01 20:41 起、持有者早已结束)⇒ 以**持有者身份**调用官方 `--release-exec`(⛔ **未删文件**),`--status` 复核=**无全局锁**;并把「无人执行时可解除孤儿锁」写进第④类跟进会话的排期 prompt(含前置判据与留证要求)。④ ✅ **修看板「完成情况」假红**:`board.py::_acc_summary` 原判据 `str(v).lower() != "pass"`,而真源写的是**中文**「过(…)」⇒ **6 条已过被判非 pass、看板显示 7/7 全非 pass**(与真源相反)⇒ 新增 `_acc_is_pass()` 认真源真实写法(`pass`/`过…`/`通过…`),现显示 **非 pass(1/7):V2**;⚠️ 改 `board.py` 须**重起才生效**(P0-17)⇒ 已重起并直读 `/board.json` 复核。⑤ ✅ 回归:`selftest` **45/0**|渲染桩 **0/61**|几何 **0/8**|`--manifest` 34 份 / 语法失败 0。前情(2026-10-02 05:1x):用户报「自动化被系统拒绝执行」⇒ 新增 P0-20(**该条的真因已于 05:4x 订正,见本条 ①**)。① 🔴 **定性(取证,⛔ 不是猜)**:拒绝的**真身**=**模型侧"拒绝受理"**,⛔ **不是**沙箱/命令黑名单 —— 证据=宿主库 `automation_runs` 表 `failure_code='automation-request-refused'` + `runResult.error={"code":-32603,"error":{"name":"refusal"}}` + **`usage` 全 0**(输入阶段就被判下,一个 token 都没烧)。历史上**仅 2 次**,全是这两条周期排期:10-02 **03:55:52**(`[唤醒]-…-脉冲`)/**04:06:37**(`[跟进]-…-队列上报`)。② 🔴🔴 **最关键的一条(决定处置方式)**:**同一份 prompt 文本** 02:47 ✓ / 03:55 ✗ / 05:06 ✓ ⇒ **阈值型/概率型**输入判定 ⇒ ⛔ **别去"找出那个唯一触发词"**(找不出、也不复现)⇒ 正解=**整体压低风险面**。③ 🆕 **`references/pitfalls.md` 新增 P0-20**:四类要换掉的措辞(按"**读起来像什么**"排序,⛔ 是语义像、不是词被拉黑)—— ① **像对抗/规避平台**(`宿主回收`/`静默 X 分钟`/`绕过`/`监管`)② **像持久化自维持**(`自维持`/`常驻`/`长活进程`/`后台 sleep`/`spawn 下一棒`/`守护`)③ **像自我繁殖**(强调"会话自己登记新的周期排期")④ **像探查平台内部**(教它直连应用库写 SQL/查平台调度表/看别的会话内部状态)⇒ 一律换成中性运维说法(「本排期每小时自动触发一次;本会话只做本轮这一遍:不重复触发、不循环等待、不新建周期排期、不起后台进程」/「取数只用 `state.py` 与上列文件」)。🔴 **最容易被漏掉的一面**:**自动化自己的记忆文件**(`.workbuddy/memory/automations/<id>/memory.md`)**下一轮会被一起读进上下文** ⇒ 那里写的"手法"**同样算输入**;本轮实测**风险最集中的不是 prompt、是这份记忆**(存着"怎么直连应用库查会话")⇒ 纪律=**记忆只写结论/数值/判据**,且这条纪律**必须同时写进 prompt**,否则下一棒会自己把手法写回去(闭环)。⛔ **禁令别写太密**(原稿近 20 处 `⛔`/`🔴🔴` ⇒ 整份 prompt 看着像一叠"约束平台"的指令)⇒ 只留 2~4 处关键的。④ ✅ **处置(已落地)**:两条周期排期 **prompt 重写**(语义一字未减:唤醒=三条判据+推一次;跟进=判断要不要开一条协作会话)+ **两份自动化记忆改写**。⚠️ 残留=被拒那两轮 `resultEvidence=none` ⇒ **那一小时空档补不回来**,只能等下一跳;另:机制层孤儿全局锁(OWNER `会话协作-机制排查与修复-20261001`,10-01 20:41 起)**仍在**,S8/S9 连续 4 轮被挡、**需人工释放**。前情(2026-10-01 23:1x):看板三处标签改「简称」(**唤醒**/**跟进** 同字号 13px/**协作**)+ 修掉校验桩两条"写死内容"的假红(退场还原判据改**与改前原文逐字比**;防回潮改**查被删那段内容**+加**防恒真对照**);回归 `selftest` **45/0**|渲染桩 **0/61**|几何 **0/8**|`--verify` 全绿。前情(同轮):2026-10-01 23:1x · 🔴 **看板三处标签改「简称」(用户第六条口径)+ 修掉校验桩两条"写死内容"的假红**。① 🔴 **口径(用户逐字)**「**唤醒会话 改为 唤醒,跟进会话 改为 跟进 字体和 唤醒一样大小\n协作程序 改为 协作**」⇒ `assets/board.html` 两处:`'跟进会话','n-title'` → **`'跟进','n-title-sm'`**(**与"唤醒"同字号** 13px,用户明令)、`'协作程序','n-title'` → **`'协作','n-title'`**(16px 沿原样);同批改布局注释 2 处 + 图外 `?` **新增简称对照**(「图上是简称:唤醒=唤醒会话 · 跟进=队列上报的跟进会话(这两格字号一致)· 协作=协作程序(⛔ 别与"**协作会话**"混)」)+ `aria-label` 同步。⚠️ **简称 ≠ 改角色** ⇒ 正文/文档/代码里的**全称一律不动**。② 🔴 **`scripts/board_ext.py` 的 `"name"` 由「唤醒会话」改「唤醒」**(图格标题取的就是它)⇒ **看板按文件签名热重载**(`board.py` 的 `spec_from_file_location`)⇒ **未重起服务**即生效,判据=直读 `/board.json` 的 `triggers[0].name == "唤醒"`(⛔ 不看页面像不像,P0-17);已同步**项目侧同源副本** `<WS>/.workbuddy/collab/board_ext.py`(md5 两边一致)。③ 🔴🔴 **修校验桩两条假红(`tmp/render-check.mjs`)—— 都是"断言写死了内容"**:**(a)** 退场用例的还原判据原写 `get('archNote').includes('已收起')` ⇒ 线上快照**真出现了**退场件时,还原后本来就该有 ⇒ **必红** ⇒ 改成 **`get('archNote') !== an0`**(`an0`=注射前原文,**逐字相等**才叫还原)。**(b)** 防回潮判据原拿**用户给的名字当子串探针**(`!includes('唤醒')`)⇒ 简称更短更常见(`[唤醒]-…`、未归类线 `唤醒机制`)⇒ **必红** ⇒ 改成**查被删那段的内容**(`/就是一条会话|外圈虚线|不进下方那一排|随需求确定时才创建/`)+ 新增**防恒真对照**(同几个词在 `?` 里**应当有** ⇒ 证明探针不是恒真)。🔴 **教训**:**"断言里写死的期望值/拿一个短词当探针"= 迟早假红或假绿** ⇒ 判据=**与"改前原文"逐字比**,或**查那段被删内容本身**;夹具 `tmp/board-verify.json` 由 22:22 旧快照(`triggers=['唤醒会话']`)**刷新为 23:07:58 线上真快照**。④ ✅ **回归(全部现算)**:`selftest` **PASS 45 / FAIL 0**|渲染桩 **FAIL 0 / 61**|几何 **FAIL 0 / 8**|`install.py --verify` **全绿**|`py_compile` 全过。⑤ 🔴 **残留(如实报)**:常驻投递**仍未重起**(载体=专用容器会话,⛔ 不在本会话起 —— 有 `pending`/`running` 后台任务会**静默压制宿主 idle 钩子**);`notify_pending=["M5=done","M7=running"]`(`line="唤醒机制"` **旧线残留**)**归档口径未定**(⛔ 不擅自丢)。前情(同轮):2026-10-01 23:0x · 🔴🔴 **用户两条指令落地:①「跟进会话」职责收敛 ② 投递「状态窗口死结」治本**。① 🔴 **口径(用户 22:5x 逐字)**「**跟进会话 只负责 ,创建协作会话(1、跟进上报后判断是否创建 2、被唤醒后 跟进目标情况 判断是否创建)**」⇒ 第④类**职责收敛为「判断+建一条协作会话」**,两条触发(**收到队列上报**/**被唤醒**)同一个动作;⛔ 它自己**不做具体活**(不改台账 `state`、不写 `blocked.json`、不派活、不抢锁)—— 那些归**它建出来的那条协作会话**。落点=**生产排期提示词已改**(`[跟进]-会话协作自检-队列上报`,改后 `nextRunAt` **复核未变=23:21:35**)+ `architecture.md §2.3.0c` + 本文件 §2 四类行。② 🔴🔴 **投递死结治本(用户报障「队列一直没有上报」)**:真因=**判据恒真** —— `sessions.status` 取值域只有 `working/completed/error/archived`(⛔ **没有"空闲"档**),**活着的**会话跑完一轮、停在等下一轮时**仍是 `working`** ⇒ 「活着⇒判忙不投 / 跑完⇒不 live 不投」⇒ **不存在能投进去的时刻**(实测 `e2ccdea3` 连打 6 分钟 `延后投递…等它空闲`+`target-busy`;旁证=唯一投成的那次目标是**日志已冻结的"死"会话**)。✅ 判据换成**宿主状态机日志** `[SessionRunStateMachine]` 的 **`busy=`**(⛔ 只写工作区日志、**不进数据库**;`AGENT_STARTED`/`RUN_ACCEPTED` ⇒ `busy=true`;`AGENT_ENDED` ⇒ `busy=false`)—— **语义不变、只换判据形态**(用户口径「**执行中就等待上报**」,⛔ **不是**"超时即投"、⛔ 没取消等待)。两道判:库里**已终结**⇒**直接判"没在跑"**(实测**自动化拉起的会话跑完不写 `busy=false`**,日志末条仍是 `busy=true` ⇒ 只看日志会把它再"忙"十几分钟)|`working`⇒再读日志**末条** `busy=` + **新鲜度**(`SRSM_FRESH=900 s` —— 长工具调用期间状态机**全程静默**,取太短会把**正在跑**的误判成**空闲**)。③ 🆕 `pitfalls.md` **新增 P0-19**,含我同轮自己踩的三处「判据看着在、其实恒假/恒真」:**(a) 正则位置组错位** —— 写成 `(?:\.(\d+))?` 时内层 `(\d+)` **仍是捕获组** ⇒ `m.group(8)` 整体**偏移一位** ⇒ `busy` **恒读 `False`** ⇒ **恒判"空闲"**(✅ 改正则**命名组** + 加**源码级反回归断言**)|**(b) 同一秒内"后出现的没胜出"** ⇒ 取到旧状态(✅ 改比 `(时间, 行序)`)|**(c) 我自己的变异法对照脚本是空的** —— 过滤词 `-k 忙判据` **没覆盖**那条 `target-busy` 用例 ⇒ **变异体根本没被试**、报"全绿"(✅ 换过滤词后 M4/M5 双双报红)⇒ 🔴 **判据:变异跑完必须回读"这个变异体到底被哪几条用例跑了"。** ④ ✅ 回归(全部现算):`selftest` **PASS 45 / FAIL 0**(新增 `t_srsm_busy` 6 项;旧 `t_target_busy` **同批改**并**补反向**「目标空闲 ⇒ ⛔ 不延后,往下走到 live 判」)|**五组变异**(打掉新鲜度/打掉终结态那道/复现错位 bug/判据恒真/判据恒假)**逐一按预期报红**、`finally` 还原后**复绿**|`install.py --verify` **全绿**|两份 `.py` `py_compile` 全过。⑤ 🔴 **残留(如实报,⛔ 不是"换判据"能解决的)**:判据换对后拦截原因**从 `target-busy` 变成 `no-follow-session`** —— 那是**真状态**(此刻**一条活的 `[跟进]` 会话都没有**:`e2ccdea3` 跑完变 `completed` 即退出网关的活会话集)⇒ **"推"只在目标活着时可用**;目标不在线时靠**排期到点拉起一条**(`[跟进]-…` 每小时)。前情(同轮):2026-10-01 22:2x · 🔴🔴 **用户报障第 3 件事(唤醒会话静默 1h20m)真因定性与治本:时钟改由宿主排期承担**。① **真因**:唤醒原本靠**会话内自维持**(`wake-pulse.sh` 一次性后台 `sleep` → 完成事件唤醒本会话)。实测:20:52 那轮起了 300 s 脉冲,**会话跑完变 `completed` 后宿主把后台任务回收了** ⇒ 之后**静默 1 小时 20 分**(`board.json` 快照里 `[唤醒]` 与 `[跟进]` 两条**都是 `completed`**)。⇒ **治本**:`[唤醒]-会话协作自检-脉冲` 由 `once` 改 **`recurring`(`FREQ=HOURLY;INTERVAL=1`)** —— **宿主就是时钟**,会话死不死都照样触发;prompt 里**明确废止** `wake-pulse.sh` 自维持那套并附实测证据,且加**幂等第 0 步**(已有活的 `[唤醒]` ⇒ 本轮让位、⛔ 不再往下做)。⚠️ 改完发现旧 `scheduledAt` 仍是 `20:52` ⇒ **上一轮那次「重排到 22:15」根本没落地**(教训:`automation_update` 之后必须 `view` 复核 `nextRunAt`,⛔ 别凭"我发过指令了"当已生效)。② 🔴 修 **`goalctl._automation_todo` 判据漂移**:它只认名字含「唤醒轮」的排期,而排期命名口径早已改成 `[唤醒]-<类别>-<具体>` ⇒ `stop`/`start` **永远打印「没找到名为唤醒轮的排期」**(判据跟着旧名字漂了)⇒ 改成**两道一起认**(旧的「唤醒轮」+新的 `[唤醒]` 前缀)。③ 🆕 `references/pitfalls.md` **新增 P0-16/P0-17**:**P0-16** 告警**每轮重写同一条** ⇒ 噪音(现场 22:06–22:12 被刷 20+ 次)+ **整体重写会静默抹掉人写的「## 解除条件」**(那是**人唯一的回信口**,21:03 手写的当场被覆盖)⇒ 两条纪律:**同句要节流**(且判据必须落在**文件**上 —— 钩子每次都是新进程,内存节流跨不了进程)**+ 重写要把人的段落原样带走**;**P0-17** 常驻服务(看板)**跑的是启动那一刻载入内存的旧代码** ⇒ **改完看不见变化**(用户报「看板中协作会话区域还是看不到协作会话」的真因之一)⇒ 判据=**直读 `/board.json` 的 `ts`/条数/每条类别对磁盘快照**,⛔ **不看页面像不像**;凡"改完没效果",先查**进程启动时间 vs 改动时间**。④ ✅ `selftest`(43→**44**) 新增 **`t_need_user_throttle_keep`(4 项)**,并按 P0-13 做**变异法**证明非空:打掉节流 ⇒ ③ 报红;打掉保留 ⇒ ④ 报红 ⇒ 全量 **PASS 44 / FAIL 0**。⑤ `goalctl stop` 补提示:**停机再起必须 `--takeover`**(看板,否则静默并存看到旧图)/**先停旧 PID 再起**(常驻),⛔ 二者都**不是热加载**。⑥ 处置常驻:22:06:15 那份 `--supervise`(pid 29944)跑的是**旧代码**(日志里还在打 22:10 已删的「前置探针」、且每 ~10 s 刷一次 `NEED-USER.md`)⇒ **已 `Stop-Process` 停掉**,容器会话重排到 **22:20**(用新代码重起)。前情(同轮早一轮):2026-10-01 22:0x · 🔴🔴 **P0-6 治本:常驻投递每次都在几分钟内被杀,真因=「释放锁=删文件」**(已实测坐实并修好)。现场读数:常驻 20:52 起、20:56 就查无此进程(全机只剩看板那一个 python);`tmp/supervise-inbox/_supervise-bg.out.log` 原文 `{"count":50,"threshold":50,"targetCount":1,"targets":[…\wake.lock]}` ⇒ **≈10 s/轮 × 50 次删除 ≈ 8 分钟**被宿主 SafeDelete 护栏终止。⚠️ 2026-09-30 那版「把 hash/最小间隔的预判提到取锁之前」**不充分** —— 它只在「锁不需要取」时才省掉删除,而真投的判据是「**内容变了**」、**内容会一直变** ⇒ 快速否决**一次都命中不了** ⇒ 每轮照删。✅ **治本**:**锁文件永久存在,释放 = 原地改写内容**(`pid` ↔ `free`)⇒ **零删除**;判据从「文件在不在」改成「**内容 + 陈旧度**」(空/`free`/mtime > 120 s ⇒ 可抢);原地写非原子 ⇒ 半截串**不算** `free` ⇒ **fail-closed**,抢锁后**回读核对**。🔴 **回归**:`selftest` 新增 ①「锁已释放(不存在 **或** 内容=`free`)」+ ②**反向**「`_deliver_str` 体内 ⛔ 不再出现 `_lk.unlink`,**且要求源码真读到**才算过」+ ③ 真互斥例「**别的进程持有时 ⇒ `locked`**,不盲投」⇒ **PASS 43 / FAIL 0**;**变异法红绿对照**(把释放改回 `unlink`)⇒ ① **仍绿**、**② 报红** ⇒ 证明"只看已释放"就是**空判据**(P0-13 同族)+ 变异体 **PASS 42 / FAIL 1**。🔴 **教训写成判据**:**「降低频率」不是修法** —— 判据是「**稳态下每轮的删除次数 = 0**」。⚠️ 新版锁文件会**一直躺着、内容 `free`** ⇒ 那**不是**残留,⛔ 别当故障去删它。⚠️ **仍未做**:常驻**还没重新起**(判据=有没有活的 `--supervise` 进程;本会话⛔ 不宜承载 —— 有 `pending`/`running` 后台任务会**静默压制宿主 idle 钩子**),载体应是专用容器会话。前情(同轮):🔴 **收尾:把「投递目标改指跟进会话」这条口径**贯通到判读层与全部文档**(⛔ 之前只改了执行层)。① 🆕 `references/pitfalls.md` **新增 P0-14/P0-15**(本轮自检自己踩的两个坑,都属「判据看着在、其实没在」同族):**P0-14** 替换注释块时漏掉结尾 `*/` ⇒ 注释吞掉后面代码(报错文字是 `SyntaxError` / `Unexpected token`,还说「多余的 `}` 在 772 行」,真病灶在 ~1000 行);🔴 教训是**判据**:⛔ 朴素「数 `{` 和 `}` 个数」**查不出来** —— 一个 `{` 从「代码」变成「注释里的字符」,两边计数照样相等 ⇒ 必须用**能识别字符串/注释/正则字面量**的扫描器(本轮 `tmp/_brace4.js`,跑备份 `depth=0` vs 当前 `depth=-1`),或最省事**改完立刻 `node --check`/`py_compile`**。**P0-15** 判「样式对不对」照**内联属性**查 ⇒ **两条假红**(`.grp` 的 `fill:none`/`stroke-dasharray` 写在 **CSS** 里,标签上根本没有)⇒ 判样式必须**回去查那条 CSS 规则**,且红绿对照时**文件路径要跟着备份一起换**(备份里没有 `.grp` ⇒ 报红正是预期)。② 🔴 **修掉一处真缺陷(判读层漏登记)**:`goalctl.py` 的 `_WHY` 表**没登记**本轮新增的三个跳过原因(`no-follow-session`/`follow-not-live`/`target-deaf`),而它的兜底是**问号占位** ⇒ 落进最后一个分支 ⇒ **打印一句「未登记的跳过原因」然后抢锁去干活**(把「没人收上报」误当成「没人干活」)。修法:三个新值登记为 **需人看**(⛔ 不降级)+ `main-not-live`/`no-main-session` 改标「**旧路径的值**」(它们现只由 `resolve_main()` 产出、**服务看板读数**;活路径里只剩在**已停用**的 `wake_round()` 里作死代码)+ **兜底由「降级」改成「需人看」**(⛔ 判读表漏登记时绝不许静默抢锁去干活)。③ **口径订正**(⛔ 都是「文档说一套、代码做一套」):`collabd.py` 用法头 `--tick` 由「**投给主会话**」改为「**投给跟进会话**」+列出真实跳过原因;`goalctl.py` 的 `wake` 段由「经协作程序**唤醒主会话**」改为「**手动补拨一次投递轮**(目标=跟进会话)」+就地标注「自动任务当闹钟**已废弃**」;`references/collab.md §4` 三类会话的**看板位置**改成现行(唤醒=**主会话下面那一行左格**、协作=**下面那个虚线大框**)+投递路由由「仍未落地」改为**已落地**+补一句「**两个虚线大框=分组**(⛔ 不是节点、⛔ 不是新一层)」。④ **位置描述订正 4 处**(`assets/board.html` 3 处注释 + `scripts/board_ext.py` 1 处)—— 原文还写着「唤醒会话贴在主会话左边」(**已被用户 2026-10-01「唤醒会话和 跟进会话 单独放一行」推翻**);`board_ext.py` 那句**保留为尺寸依据**并就地标注「位置这半句已作废」⇒ 同时把**项目侧同源副本**(`.workbuddy/collab/board_ext.py`)改到**逐字节相同**(`diff -q` 复核)。⑤ **回归(本轮末次)**:`selftest` **PASS 42 / FAIL 0**(连跑两轮同结果)|渲染桩 **FAIL 0/56**(live)|几何 **FAIL 0/8**|`install.py --manifest` **34 份文件 / 语法失败 0**|全包 **15 份 `.py` `py_compile` 全过**(=P0-12 那条判据当场再跑一遍)|两份任务图 JSON `json.loads` 全过。⚠️ 本次改的是**技能包内部** ⇒ 看板 HTML 由 `board.py` 按 `HERE.parent/assets/board.html` **实时读盘**(⛔ 无副本、⛔ 不用重装),但**页面要刷新**才看得到。前情(同轮前半):2026-10-01 21:5x · 🔴🔴 **看板协作架构图「分组大框」改版(用户六连改)+ 投递目标改指跟进会话的机械落地**。① **布局(用户逐字,按序落地)**:(a)「可以把主会话 这个框图 分左右两部分…把**主会话 跟进职责**放给跟进会话了」→(b)「**换新会话 唤醒的是 跟进会话,主会话只能是用户触发**」→(c)「**唤醒会话和 跟进会话 单独放一行**」→(d)「**唤醒主会话 改为 唤醒会话**」+「**唤醒会话 和 跟进会话 框一样大小**」→(e)「主会话的框 **上移**一些」「**增加连接 唤醒会话 和 跟进会话的 实线**」→(f)「先等等 有个办法更好,用一个**虚线大框**把 主会话 唤醒会话 和 跟进会话都框起来,这个虚线大框 **连接 协作会话 虚线大框**就好」。⇒ 现在图上:**两个虚线分组大框**(`.grp`:虚线·不填色·**⛔ 不是节点、也⛔ 不是"新一层"**)—— `UA` = 主会话+唤醒+跟进(尺寸**从三格包围盒现推**,⛔ 不写死坐标)、`UB` = 协作会话那一排(**按该排布局现推**,贴内容、⛔ 不铺满整幅);两组之间**只有一条线 = ① 派活**(**取代**原来"主会话往每一格射线"的扇形+不再穿过 RW 行)。顺手消掉两个麻烦:① ① 不必再穿过 RW 行去找格子、② 唤醒 → 跟进那条实线**再没有竖线与之十字相交**(用户要的"实线"落到实处)。`viewBox` `0 20 1280 996` → **`0 20 1280 1054`**。② **投递目标改指跟进会话**(`_deliver_str()`):新 `follow_for_topic()` 按类别取**跟进会话**;解析不出 ⇒ **喊用户**(`skipped=no-follow-session`/`follow-not-live`),⛔ **不再降级投主会话**(主会话是**用户那一面**,机制噪音会挤掉用户的话 —— 实测把它压成 `parkInQueue`「只进不出」);投递台账加 `target:"follow"`,`_main_sid()` 加**防污染**判(否则"上次投给谁"会把**跟进会话**回读成主会话)。③ **新增 `_scan_follows()`**(扫本工作区全部 `[跟进]-…`,按类别各挑一条、同类取最近活动,只认**活着且不哑**,⛔ 绝不返回主会话)。④ **`board.py::_sessions()` 截断改成"四类各保底一条"** —— 每类都有自己的位置,跟进会话被截掉 ⇒ 那格渲成「(未登记跟进会话)」=**假话**。⑤ **回归(全部现算,⛔ 不写死坐标/期望值)**:渲染桩 **0/60**(默认快照 0/56、双目标 0/59)、几何 **0/8**(新增 ⑦ 段:两框包住各自内容/互不重叠/不填色·虚线 **查 CSS 规则**)、自测 **PASS 42 / FAIL 0**(**连跑两次**查跨轮假红);**四个变异体**(删框/① 改回从主会话底边出发/上框缩窄 80px/下框上移 10px)**逐一报红** ⇒ 新断言不是恒真;红绿对照(改前备份)**8 红**。🔴 本轮自检自己踩了两个坑(已修+已写进源码注释):**(i) 替换注释块时漏掉结尾 `*/` ⇒ 整段注释吞掉后面代码**(报错文字是 `SyntaxError` / `Unexpected token`,而**"括号数相等"的朴素统计查不出来** —— 因为一个 `{` 从"代码"变成了"注释"里的一部分 ⇒ 判据必须用**能识别字符串/注释/正则的扫描器**,⛔ 不能用字数统计);**(ii) 判"是不是虚线/有没有填色"照 `<rect>` 内联属性查 ⇒ 两条假红**(`.grp` 的样式写在 **CSS** 里)。⑥ **同步改动**:`assets/board.html`(CSS + 布局常量 + `UA`/`UB` + ① 改分组连线 + `?`/`aria-label`/顶部布局注释)、`scripts/board.py`、`scripts/board_ext.py`(**唤醒主会话 → 唤醒会话** 14 处 + 位置描述 4 处)、`scripts/selftest.py`、`references/architecture.md`(§2.3.0c **删掉「降级投主会话仍未落地」**+新增**投递路由表**与 **§2.3.0c-2 分组大框**)、`references/collab-detail.md`(形状/位置/名称订正 + §0.5.5 补"位置改过一次")。前情:2026-10-01 20:5x · 🔴 **第④类「队列上报的跟进会话」落地 + V3「闸门齐全」真因修复 + 开工清单写进五载体**(用户:「把协作机制的问题排查和修复也当作目标一起完成」)。① **第④类落地**(`[跟进]-<类别>-<具体>` ⇒ `follow`):改齐**四处同款** —— `collabd.py::parse_session_name()` 角色表 + `board.py::_role_of_title()` 镜像 + **两边的主会话候选排除元组** `("worker","waker","follow")` + `selftest.py` 逐样本对账(**3 项 → 4 项**)。⛔ 漏了它 ⇒ `[跟进]-…` 的 `role` 解析为空 ⇒ 被 `_scan_mains()` 当**主会话候选**(标题带 `[<类别>]` 时还会变成"该类别的主会话")⇒ **通知投给它自己**(与接续棒同款的自指死结)。② 🔴🔴 **同批修掉一个「一直存在但看不见」的缺陷**:`board.py` 原来把会话角色**二分**(不是主会话就标「协作会话」)⇒ **唤醒会话与跟进会话都被标成「协作会话」** ⇒ 看板第三层(按 `role` 挑格子)把它们**误画进协作会话那排**(唤醒会话**画两遍**、跟进会话**顶错名字**)—— **它一直看不见,因为这两类会话从来还没被真的建出来过**。修法:新增 `board.py::_role_label()` 输出**四类**,`board.html` 第三层判据 `role !== '主会话'` → **`role === '协作会话'`**(两边必须同款)。回归:渲染桩 **0/48**(双目标 **0/51**)、几何 **0/7**、`--verify` 全绿。③ 🔴 **V3 的真因**:`collabd --where` 的 `TG` **来自 `.workbuddy/collab/collabd.config.json`,⛔ 不是 `goal.json.taskgraph`**(两处都写了,**以配置为准**);而它指的是**手机接入线**的任务图(M1–M7 **全 done**,该线 09-30 已退役)⇒ **可派集合恒空** ⇒ 队列 0 件 ⇒ `NEXT.md` **永不产生** ⇒ 四道闸门第一道就断。修法:新建 `交付物/任务图-会话协作自检.json`(S1–S7,`line` 取当前类别)+ **配置与 `goal.json` 双改指**。④ **开工清单**(用户明令「主会话开工时必须建齐 唤醒/按类别协作/跟进」)写进 **五载体**:本文件 §2 + `architecture.md`(**§2.3.0c 改「已落地」** + **新增 §2.3.0d**)+ `collab.md §4.1` + 项目 `CODEBUDDY.md §F`(**列为第 3 类免确认白名单**)+ 项目 `MEMORY.md` 行。⑤ 🆕 **`references/pitfalls.md` 加 P0-12**:写 `.py`/`.json` 时嵌 ASCII 双引号 ⇒ 截断字符串/破坏 JSON 且**多半静默**(吞异常的调用方 `rc` 仍是 0)—— 本日**犯 3 次**(`board_ext.py`/`selftest.py`/任务图 `notes`)⇒ **判据:落盘后立刻 `py_compile`/`json.loads` 验一遍,⛔ 不靠"我看着对"**。前情:2026-10-01 20:3x · 🔴 **删掉看板主会话框里那行「负责类别:—(未在对话里说明)」(判据:它会说出假话)** —— 用户指着它问「**这个是什么意思,如果没用就删掉**」。查清后判**该删**,三条依据(均为实测):① **它会说出假话** —— 快照里 `topics_source.kind='declared'`(类别 20:25 在对话里说明过)而 `main_by_topic` 为空时,这行渲染成「未在对话里说明」,**把"这条主会话没对应到任何类别"说成了"类别没声明过"**(两件事被一个判据混在一起);② 同一事实**版面上已说过两遍**,且方向更对 —— 顶部「本项目」卡的**任务类别 chips**(每个 chip 带「主会话 `<id8>`」/「⚠️ 无主会话」,`?` 里还有来源与时间)+ 图外说明「当前」块第 1 条 ⇒ 那行是**第三遍**、还是**反方向**(主会话→类别);③ 用户反复要求「看板要精简的展示重点有效的信息,不是写说明书」。↩️ 本次**推翻了 2026-10-01 用户自己那次摆放**(「唤醒机制如果是主会话的 事就放到主会话框里去」)⇒ 已在源码注释与 `references/collab-detail.md` §⓷ 写明**谁删的/为什么删/怎么还原**(还原点 `tmp/bak-负责类别行-20261001/board.html.bak`)。配套:`R2.h` 112 → **84**(回落到加那行之前的高度;`tmp/arch-geom-check.mjs` 的 `rows.R2` 镜像必须同步,⛔ 否则几何自检=假绿);**顺带治同源假话** —— 图外说明「当前」块第 1 条原按 `TOPS.length` 判,而 `TOPS` 走 `_goal_topics()`、**会把目标简称回落成一个类别** ⇒ 从没说明过时也恒 ≥1,于是把**回落值当成已声明的类别**报出去、同时「⚠ 未在对话里说明过」成了**死分支** ⇒ 改按 **`topics_source.kind`**(与顶部 chips 同源,⛔ 不再各说各的);3 处陈述同一事实的注释同步纠正。回归:渲染桩 **FAIL 0/47**(双目标样本 0/50)、几何 **0/7**、`install.py --manifest` 34 文件 / 语法失败 0、`--verify` 全绿(10 钩子 rc=0 + 自测 PASS 39 / FAIL 0)。🔴 `tmp/render-check.mjs` 里原先那条**正向断言**(「主会话框写出『负责类别』」)已**反向**为 3 条(⛔ 不是改写成恒真的空断言 —— 那是假绿),并用**改前备份**做了红绿对照:线上真实快照 改前 **6 红** → 改后 **4 红**、合成 `kind='fallback'` 样本 改前 **7 红** → 改后 **4 红** ⇒ 三条新断言都是**真判定**(残留的 4 条是写死给样本快照的内容型断言,与本次无关)。前情:2026-10-01 20:1x · 🔴 **看板按最新架构调整(常驻口径回归)+ 用协作机制跑了一次自检** ⇒ 看板 `assets/board.html`/`scripts/board.py`/`scripts/board_ext.py` 三处把 09-30 的「钩子按需唤起/跑完即退/不是常驻进程/没有启动-停止这回事」全部改回**常驻投递**口径(用户 2026-10-01「**协作与投递一直运行(常驻)**」+「**定时任务的方案已经废弃了**」);`prog_by` 不再猜调用方(戳上分不出常驻还是钩子)⇒ 只报轮次名;帮助文本补**四类会话 + 接续=形态**。回归:渲染桩 **FAIL 0/45**(双目标样本 0/48)、几何 **0/7**、`--verify` 全绿。🔴 **顺带修掉 4 个真缺陷**:① `preflight-lock.sh` **未登记技能包** ⇒ 改技能包一律被误判【D】未归类、报"先定域"(正解是"独占")⇒ 任何人改技能包都开不了工(**原位修,包内+库内两处同源**);② `goalctl.py` 工作区解析按 `__file__` 推层级 ⇒ 不带环境变量时**静默指向技能包上级目录**(读数全"无"、还会在技能目录下建 `tmp/`)⇒ 改为 `DSH_COLLAB_WS` → `DSH_WS_ROOT` → cwd,落空时**醒目告警**;③ 同文件加**静默陷阱告警**(换目标名不给 `--kpi` ⇒ 旧验收判据原样留着 ⇒ 新目标一声明就被判"全过");④ `state.py` 抢锁路径提示写错(`scripts/` ⇒ `07-scripts/`)。前情:2026-10-01 19:5x · 🔴 **会话类别口径修正(用户订正 · 三→四类)**:由「三个会话角色」改为 **四个会话类别**(**① 主会话 ② 协作会话 ③ 唤醒会话 ④ 队列上报的跟进会话**)+ 立下 **「接续会话 = 形态、⛔ 不是第 5 类」**(撞阈值时产生,**角色继承被接续的那条**)⇒ 改齐 `SKILL.md`(description/§2 铁律)· `references/collab.md §4` · `references/collab-detail.md`(三处:命名前缀/第四形态/**§12「主体」与「会话类别」两轴消歧**)· `references/architecture.md`(当前结论表/§2.3 第1级角色/**新增 §2.3.0c 第④类**)· `references/pitfalls.md`(**新增 P0-11**)· 项目 `MEMORY.md` 对应行。前情:2026-10-01 19:0x · **新增 `install.py --manifest`**(把「重算 `references/manifest.md` 逐文件表」固化成子命令 —— 包一改表就过期,以前每轮临时手搓脚本、跑完即删,已因此误判两次)+ 清掉最后两处脚本盘符字面量(`lock/preflight-lock.sh`、`hooks/lock-guard-hook.py`)+ 🔴 修 `selftest.py` 一处**静默假绿**(`t_tick_wired` 写死项目绝对路径 ⇒ 路径搬迁后恒走"文件不在,跳过"、看着绿其实没验;改按包内相对路径取自己的钩子)。更早:2026-10-01 18:5x **外置根贯通到全部脚本**(引导块 9 → 16 份)+ 7 份加输出编码兜底 + 修「钩子把技能包当工作区」与「重定向 + GBK ⇒ `print` 抛异常 ⇒ 整轮 `fatal`」;14:5x **建包**(由 `multi-session-collab` + `workbuddy-session-forensics` 合并;`install.py` 声明表/`--dry-run`/`--verify`;根目录外置 `roots.env`)。
agent_created: true
---
# session-mechanism — 会话机制(含多会话执行)
> 🔴🔴 **2026-10-03 07:2x 口径改(本条为准,排在 10-02 那条之上)——「上报」整套退役 + 看板两处几何改动**
>
> 用户逐字:「**没用了就删除,现在的机制是 协作程序接收和处理队列**」。
>
> **一、现行机制只剩两条腿(都已实测活着)**:
> · **接收** = `--report` 把协作会话的执行状态写进 `tasks.json`(四态台账,唯一权威)—— `task_report()`。
> · **处理** = 建**检查会话排期**(`maybe_spawn_check_agent()`,四道闸)让会话去读队列干活
> —— 常驻 `--supervise` 每 2 轮判一次,是这条腿的**唯一载体**(⛔ 检查会话只能靠排期开)。
> · ⛔ **没有"推送"这一环**:队列变化**不再自动通知任何人**。要落一件事,**显式建一条协作会话**。
>
> **二、本轮真删了哪些(⛔ 不是"标成已退役")**:
> · `collabd.py::supervise()` 里的 **①′待反馈序列 + ②③单条握手 + ④唤醒** 四段,
> 以及两个 `_deliver_str()` 调用点 —— **203 行 → 71 行(-132)**,备份 `collabd.py.bak-投递退役-20261003-0700`。
> · `--tick` 里的 `check_delivery_consumed()` 调用(判据 `st["wake"]["expect"]` 只由 `_deliver_str()` 写
> ⇒ 投递删后**恒返回 `{}`**)。
> · 看板「**最近上报 `wakeups.jsonl`**」整张卡(该文件**已不再被写**、实测根本不存在 ⇒ 恒空假面板)。
> · 看板两处几何改动,**都是用户当面纠正后落地的**(⛔ 不是我第一版那么写的):
> ① 架构图 ④ 那格 —— 我第一版把「上报」**改名成「常驻」**,用户逐字驳回:
> 「**怎么又把上报 改成常驻了 常驻什么,不是 协作程序常驻吗**」。
> **根因=把「角色名」与「运行形态」混成一个词**:那一格的角色本来就叫**协作程序**(`collabd.py`),
> "常驻"只是它的一种跑法(`--supervise`)—— 用模糊词替代具体词 = 越改越糊。
> ✅ **处置=两格合并成一格**(`PX=280, PW=720`;删 `UX`、删随之悬空的 `RKX`、`TICKX` 改算为 `PX+PW/2+120`):
> 标题「**协作程序**」/副标题「**接收:--report 写台账 · 处理:建检查会话排期 · 常驻一直运行**」
> /右侧小字「⛔ 队列投递已于 2026-10-03 退役(曾叫「上报」)」。
> ⚠️ 备份 `board.html.bak-两格合一-20261003-0720`;删变量在**代码行**零引用(自写检查器 `tmp/_check_board_js.js`)。
> ② 「最近上报 `wakeups.jsonl`」整张卡删除(该文件**已不再被写**、实测根本不存在 ⇒ 恒空假面板)+其渲染块。
> · 另 4 处用户可见「上报」文本替换:`② 上报 · --report` → 「② 写台账 · --report」(空态/有会话两处);
> `Hook进程` 框内 `--tick(上报)` → 「--tick(补检查排期)」;台账空态文案 → 「用 --report 把状态写进这里(四态…)」;
> 协作程序格副标题「只维护不上报」→「**只维护不推送**」。
> · `collabd.config.json`:`wake_enable` **true → false**(第二道闸,第一道是代码里已删那两段)。
>
> **二之二、🔴 顺带查清的判据:「改看板要不要重启」有四类答案,混成一句话就是误导**
> (用户第二句纠正:「**看板每次修改都不处理看板**」⇒ 我把"改配置要重启"说成了"改看板都要重启",**是分类错误**):
> · 改 `assets/board.html` ⛔ **不用**(`board.py:1293` `serve()` 里 `html_p.read_bytes()` **每请求实时读盘**;
> `/board.json` 带 `html_sig`(`mtime.size`)⇒ 页面自动重载)—— 现跑实证 `tmp/_probe_hotreload.py`:
> 改 `<title>` 插标记 → ⛔ 未重启 → 页面立即含标记(md5 已还原一致 ✔)。
> · 改 `.workbuddy/collab/board_ext.py` ⛔ **不用**(`EXT_CACHE` 按 `(mtime,size)` 签名**热重载**,`board.py:855-870`)。
> · 改 `scripts/board.py` 自身 ✅ **必须**(进程里是启动那一刻载入的旧代码)。
> · 改 `collabd.config.json` ✅ **必须**(`C = _cfg()` 在 `board.py:81` **模块级执行一次**,⛔ 无 re-read 路径)。
> ⇒ **判据=先答"这个文件是谁在读、什么时候读"**;⛔ 别把"改了 X"与"改看板"当同一件事。
> ⇒ 全文 ⇒ `references/pitfalls.md` **P0-38**(与 P0-17/P0-33 同族但**方向相反**:那两条讲"该重启却没重启",
> 本条讲"⛔ 不该重启却去重启了" —— **重启不是万能药**)。
>
> **三、🔴 删除前的实测读数(这才是"为什么该删"的证据)**:
> | 读数 | 值 |
> |---|---|
> | 日志 `follow-retired` 行数 | **1 313 行且每 20 s +1**(每轮试投、每轮失败) |
> | `notify_pending` | `["S8=done","S9=done","S5=done","S12=done"]` —— **4 条全 `done` 堆在队首** |
> | `wakeups.jsonl` | **文件不存在**(投递早已没成功过) |
> | `TO-MAIN.md` | 每轮被覆写成 `S8 -> done`(那份通知永远送不出去) |
> | `NEED-USER.md` | 每轮刷新「投递链路没有收件人」 |
>
> **四、🔴 它是三重死锁,不是一条腿断**:
> ① 投递失败 → `collabd.py`「**只有真投出去才允许消费队列**」⇒ 不消费;
> ② 不消费 ⇒ 队首永驻一条投不出去的死件(`S8=done` 从 23:55 堵到 07:08);
> ③ 队首非空 ⇒ `no_fb` 恒 False ⇒ **唤醒的条件②「队列无待反馈」永不成立** ⇒ 唤醒那条路跟着一起死。
>
> **五、⛔ 保留的零调用点函数(本体不动,是"将来重建收件人"的实现)**:
> `_deliver_str()` / `follow_for_topic()` / `wake_round()` / `check_delivery_consumed()`。
> 🔴 **为什么不删**:`selftest.py` 有 4 处**真调用**前两个 ⇒ 删函数体 = `NameError` 崩自测。
> ⚠️ **判据必须是 AST 而不是字面 grep** —— 退役说明的 docstring 里**必然**提到被删的函数名
> ⇒ 任何 `grep "_deliver_str(" in <函数体切片>` 都**永久假红**(见 `selftest.py::_calls_in()`)。
>
> **六、验收(全部现算)**:`py_compile` 4 份全过 | `board.html` 两个 script 块 `new Function()` 全过
> | **`selftest` PASS 47 / FAIL 1**(回到基线;那个 FAIL 是域锁锚点词表的项目名,⛔ 既有问题)
> | **60 秒观察窗内 `follow-retired` 增量 = 0**(改前每 20 s +1)
> | 常驻新实例 pid 49324、心跳 `round` 递增、心跳 JSON 里 `wake` 键**已消失** ✔
> | 看板 `--takeover` 后 `/board.json` 的 `meta.deliver_retired == "2026-10-03"` ✔
> | Edge headless 截图(`tmp/board_merged.png`)实证:④ **一格「协作程序」**+「队列 2 件 · 已完成 2 · 未完 0」
> +「接收:--report 写台账 · 处理:建检查会话排期 · 常驻一直运行」+ 右侧退役小字;
> `--once`(x=440) 与 `--tick`(x=640) **两条线都落在框内**;**「最近上报」卡已消失** ✔
> | 两格合一后**再跑一次 `selftest` 终检**= PASS 47 / FAIL 1(与合并前同基线,⛔ 合并没引入新问题) ✔
> 🔴🔴 **2026-10-02 23:4x 口径改(本条为准,全文其余「四类」表述按此读)**
>
> **会话类别从四类收敛为两类**:① **主会话** ② **协作会话**。
> **唤醒会话 / 跟进会话 / 队列上报机制 —— 整套退役**(用户逐字,三句):
> ① 「按照之前的讨论 唤醒会话 跟进会话 和 上报程序 都去掉才对」
> ② 「上报机制也不需要了」
> ③ 「按照新的逻辑整体修改」
> (口径**早就在** `scripts/collabd.py:2293` 写着:「创建检查会话的为 协作程序
> **(现在不需要上报机制了、之前已经去掉 唤醒会话和跟进会话机制)**」—— 本轮是**把代码与文档对齐到已有口径**。)
>
> **已落地(现算)**:
> · 角色表 `collabd.py::parse_session_name()` 与 `board.py::_role_of_title()` **两处同款**收到 `主/协作`
> ⇒ `[唤醒]`/`[跟进]` **判空**(旧 `sessions` 行不删,但不再是活类别,也**不会被当主会话候选**)。
> · 主会话候选排除元组**两处**收到 `("worker",)`。
> · `follow_for_topic()` **短路退役**(恒返回 `why="follow-retired"`、`sid=""`)⇒
> ⛔ **不投主会话**(主会话只由用户触发),⛔ 不盲投。
> · `goalctl.py::_WHY` 登记新档 `follow-retired`(「需人看」)⛔ 不降级去抢锁自己干。
> · 两台周期钟 `[唤醒]-…-脉冲` / `[跟进]-…-队列上报` + 3 条一次性跟进排期 ⇒ `ACTIVE→PAUSED`、
> **零删除**、`integrity_check=ok`、周期排期未误伤。
> · **检查会话由协作程序建**(`maybe_spawn_check_agent()` 四道闸),⛔ 不经唤醒/跟进转手。
> · `selftest` **PASS 47 / FAIL 1**(那个 FAIL 是域锁锚点词表里的项目名,⛔ 既有问题、⛔ 故意不动)。
>
> ⚠️ **旧正文里「四类」「跟进会话」「唤醒会话」的大量表述按本块读** ⛔ 不是"没改",
> 是**刻意保留**(那是历史证据与踩坑记录;与本块冲突处以本块为准)。
> 🔴 **`board_ext.py` 里的「链路前置」已停用**(讲的是 2026-09-30 随概念退役的手机接入线 20090 链路)
> ⇒ `collabd.config.json` 的 `board_ext` 指向 `.RETIRED-20260930.py`。
> ⚠️ **改了 `collabd.config.json` 必须重启看板才生效** —— `C` 是**模块级加载一次**,
> ⛔ 不每次快照重读(这是 P0-17「改了看不见」漏记的一面)。
> 🔴 **一句话**:**会话要先活得下去(钩子 + 锁 + 日志闸),再谈协作(派活靠自动化、收结果直读宿主库、机械判定下沉、人只看一个看板)。**
> ⚠️ **包自包含**:所有脚本、参考件、资产都在本包内(`scripts/` `references/` `assets/`),⛔ 不再依赖文档库 `07-scripts/`。换机器 = 拷本包 + 跑一次 `install.py`。
---
## §0 怎么用(先读这一节)
**① 我要装 / 要换机器** ⇒ 跑 `python install.py --dry-run` 看 diff,再 `python install.py --apply`,最后 `python install.py --verify`。
(`--apply` 会:写 `roots.env` → 按**声明表**接线全局钩子 → 初始化工作区 → 记 `install.log`。⛔ 不硬编码 python 路径与盘符。)
**② 我要查会话机制** ⇒ 读 `references/architecture.md`(唯一权威:机制全貌)+ `references/rules.md`(规矩与判据)。
**③ 我要查多会话执行** ⇒ 读 `references/collab.md`(执行四条通道 + 派活模板)。
执行检查可独立使用:`python scripts/collabd.py --where` / `--tick` / `--report`。
**④ 我要复盘某个历史会话** ⇒ 读 `references/forensics.md`,取证脚本 `scripts/forensics/proc-parent.py`。
**⑤ 踩过坑 / 要避坑** ⇒ `references/pitfalls.md`;**包内文件清单与来源** ⇒ `references/manifest.md`(逐文件 md5 + provenance)。
**⑥ 我要让执行检查长期在线(⛔ 会话/工具调用起的活活不过当轮)** ⇒ 读 **`references/supervise-persistence.md`**(唯一权威:Windows 计划任务 + 守护循环 · 三条封死路 · 三个秒退坑 · `LastTaskResult` 验收)。
 🔴 **先分场景再动手**(该文档 §〇 有对照表):**用户手动创建主会话** / **定时任务创建主会话** —— 两者都要**同一个载体**(计划任务 + 永不返回的守护循环);⛔ **排期代替不了载体**(跑完即 `completed`,下一跳之前是空窗);⚠️ `status=ACTIVE` **≠ 在跑**(`once` 排期过期即哑:`next_run_at=None`)。
---
## §1 第一段 · 会话机制
> 这一段**不依赖**多会话协作;只把「会话活得下去」这套装上。
**三件套**
1. **钩子**(`settings.json` 全局生效,`scripts/hooks/` 6 份):日志闸(`session-log-guard.py`)/锁闸(`lock-guard-hook.py`)/输出闸(`bash-output-guard.py`)/叫停闸(`stop-dialog-guard.py`)/技能闸(`skill-load-guard.py`)/结果回报(`wb-result-hook.py`)。
2. **锁**(`scripts/lock/`):
- 开工三步=`scripts/dsh.py open`(本项目入口,内部跑 ①状态 ②preflight ③抢锁);
- 抢锁 `handoff-guard.sh --claim-exec "<会话名>" [--domains <域>]` —— **⛔ 机制层必须独占(不带 `--domains`)**;抢不到 ⇒ **停手 + 报告**(红线 R9:⛔ 不删锁、不接管)。
- 释放必须反序:`--release` → `--release-exec "<会话名>"`;⛔ 不带名 ⇒ 拒释放。
3. **日志闸**:文件软 **5** / 硬 **8 MiB**;工具调用软 **200** / 硬 **250**。命中 ⇒ 开接续会话。
**🔴🔴 执行会话「独立域」硬规则**(用户 2026-10-02 定案,逐字)
> 「创建协作会话还要加个判断:协作会话必须是独立域运行的,就是做所有修改操作都在单独的文件下运行
> (比如某个项目要开发 webserver,desktop,phone app,独立插件或产品原型)这些文件都可以放在工作区对应
> 独立文件夹下,**只能只读的方式访问别的文件夹内容**。应为会话有锁的机制,开多个会话都操作一个域的文件
> 只有一个会话能执行,别的只能干等。」
- **域目录=工作区下的第一层目录**(`<工作区>/webserver/`、`<工作区>/desktop/`…)。
🔴⛔ **绝不许套公共父目录**(`domains/xxx`、`projects/xxx` 那种)—— 实测那样所有子目录会算出
**同一个域键** ⇒ 域锁等于没有、并行直接失效(本机制实测踩过并已改正)。
- **域键算法只认第一层**:域键=`<工作区名>/<第一层目录名>`,与再往下钻几层**无关**。
⇒ 想让两个会话真并行,就给它们**两个不同的第一层目录**;⛔ 在同一目录里再分层**不能**解锁并行。
- **开工第 0 步必须先抢域锁**:`handoff-guard.sh --claim-exec "<会话名>" --domains "<域目录>"`;抢不到 ⇒ **停手报告**,⛔ 不许硬写。
- **写只许在域目录内,跨目录一律只读**;必须写到外面时 ⇒ **不写**,在 `tmp/supervise-inbox/NEED-USER.md` 写明要谁批准。
- 三条命令(`collabd.py`):
```bash
python "<包>/scripts/collabd.py" --domain-status # 域现状体检(在册域锁 + 锚点词表一致性)
python "<包>/scripts/collabd.py" --domain-suggest "<类别>" # 推荐一个**当前空闲**的域名
python "<包>/scripts/collabd.py" --domain-check "<域名>" # 判这个域能不能派(⛔ 被占时 rc=1)
python "<包>/scripts/collabd.py" --domain-block "<域名>" # 打出派活要嵌的门禁块,⛔ 别手抄
```
- **门禁已自动嵌进派活**:执行会话 prompt(`GAP_PROMPT["worker"]`)与检查会话 prompt(`CHECK_PROMPT`)都带上了这段,
⛔ 跟进/唤醒**不带**(它们只读+建排期,给域目录是反向约束)。
**关键判据(踩过才写在这)**
- 🔴🔴 **用这套技能的第一件事=查环境配置,配没配决定后面全部动作**(用户 2026-10-02 定案:「重点是**技能的使用时要检查环境配置是否已配置,如果没有配置就要先配置**」)。
```bash
# 体检(只读,⛔ 无副作用)—— 判技能库根能否定位 + 已注册钩子逐条目标是否存在
python "<包>/scripts/hooks/_env.py" --ws "<工作区绝对路径>"
# 写环境标记(状态变量+时间),scope 决定落在哪个文件夹
python "<包>/scripts/hooks/_env.py" --scope global --stamp --fixed "<修了什么>"
python "<包>/scripts/hooks/_env.py" --ws "<WS>" --scope workspace --stamp
```
- **作用域要问用户**(用户 2026-10-02 原话:「可以询问是配置在**全局**还是**本工作区**」)——
🔴 **默认问、别默认写**:**全局**=一次配好所有工作区共用(改它影响**所有**工作区 ⇒ 属影响面变更);
**工作区**=只管本工作区(换机器/别的项目要各自重配)。
判据:影响面超出本工作区 ⇒ **必须问**;纯本工作区 ⇒ 自决并**一句话说明**。
- 标记落点:全局 ⇒ `<配置目录>/env-stamp.json`;工作区 ⇒ `<工作区>/.workbuddy/env-stamp.json`。
字段=`scope`/`checked_at`(ISO 到秒)|`ok`|`config_dir`|`skills_root`|`hooks`(逐条存在性)|`fixed`。
- 🔴 **定位不到技能库根 ⇒ 报错 + 非零退出,⛔ 绝不「静默零输出」**(2026-10-02 实测事故的病根):
`reply-style-guard.py` 被注册成文档库里的旧副本 ⇒ 沿 `__file__` 上溯够不到 `skills/`
⇒ 落进 `os.path.expanduser('~/.workbuddy/skills')`,而 **Windows 上 `~` 不是真配置目录**(真值在
`CODEBUDDY_CONFIG_DIR`)⇒ **静默零输出**,日志只留一行 `core=0 字符`
⇒ **「机制坏了」与「没配规则」表现完全一样**(真实代价:另一工作区为此绕了两轮,在「规则文件在不在」上打转)。
⛔ `~` 回落已从 `_skills_root()` 删除;**每一档 env 都要验目录真存在**(env 给错要继续往下找,⛔ 不猜)。
- 工作区 `state.py` 已有 `§5c [钩子环境]` 一项跑它 ⇒ **开工跑状态就能看见**,⛔ 不必另记命令。
- 🔴 **各脚本的根目录一律先读包内 `roots.env`,再回落按位置推导**(已贯通 **16 份**:9 钩子/锁 + 7 非钩子脚本)—— 因为 `settings.json` 的 hook 条目**没有 `env` 字段**,包内脚本**搬一次就会静默指错**(历史事故:台账写到别处、测试却全绿;近期又复现一次:`wb-result-hook.py` 按 `__file__` 推三层 ⇒ **把技能包当工作区**,在包里长出 `tmp/supervise-inbox/`)。**⛔ 不要靠 `mv` 搬迁,⛔ 不要靠"改壳转发"**(转发会改 `$0`,同样挪走根)。**⛔ 代码里不留盘符字面量**:外部根只能来自 `roots.env`/宿主 env。**🔴 2026-10-02 起再收一层**:「配置目录/技能库根」的取法统一走 `scripts/hooks/_env.py`(7 个钩子已改),⛔ 各脚本不再自己拼 `~`。
- 🔴 **出口一律声明编码**:钩子脚本写 stdout 走 `sys.stdout.buffer.write(bytes)`(文案含 `⛔` ⇒ 否则 `UnicodeEncodeError` ⇒ stdout 空 ⇒ **静默放行**);**非钩子脚本**(`collabd` / `board` 等,会被重定向到文件或 `DEVNULL`)在文件头加**输出编码兜底**(`sys.stdout/stderr.reconfigure(encoding="utf-8", errors="replace")`)—— 实测:重定向 + 本地 GBK ⇒ `print("⛔…")` 抛异常 ⇒ 被顶层 handler 记成 `fatal`、**整轮失败**(常驻必踩)。
- ⚠️ **本机**:裸 `bash` 可能落到 WSL 启动器 ⇒ 要跑 shell 一律显式用 `PortableGit/.../usr/bin/bash.exe`。
- 🔴🔴 **钩子总预算(2026-10-02 事故)**:**钩子超时 ≠ 钩子变慢,而是用户这一句话被拦下**(`UserPromptSubmit operation blocked by hook: Hook timed out after 20000ms` ⇒ 提交失败,不是慢)。根因=**`UserPromptSubmit`(宿主注册 20 s)上一个脚本里串了三个子进程**:`collabd --gap` **13.5 s** + `--once` 0.5 s + `--tick` **13.5 s** ≈ **27.5 s** ⇒ 必超。四条硬规则(已落进 `wb-result-hook.py`):
1. **开局认领预算**:`BUDGET = {UserPromptSubmit: 18, PreToolUse: 25, SessionEnd: 8}`(各比注册值少 2 s)⇒ 任何**要等**的子进程,先问 `_left()` 够不够,不够 ⇒ 跳过并留痕。
2. **门槛按"实测耗时"给,⛔ 不按硬超时给**:`sweep`/`supervisor` 实测都是 **0.95 s**,若按硬超时 8 s/6 s 设门槛,在 8 s 预算下**永远跑不到**(判据看着在、其实恒假)。
3. **不需要结果的活 ⇒ 后台**(`_bg()`:`Popen` + `DETACHED_PROCESS|NEW_PROCESS_GROUP` + stdout **落文件**,⛔ 不用 PIPE):实测**父进程退出后仍能跑完**(15 s 写完 11 KB)。⛔ `CREATE_BREAKAWAY_FROM_JOB` 在本机**必失败**(`PermissionError 13`)⇒ 别加。
4. **一轮只允许一个贵活**(`_HEAVY_DONE`),其余后台;**慢的产物落缓存**(`gap-cache.json`,后台写 `.tmp` → 下轮 `os.replace` 收割,钩子只读缓存=毫秒级)⇒ 实测钩子 **27 s → 0.5~1.9 s**。
⚠️ 附带发现:`SessionEnd` 注册只有 **10 s**,而它上面挂着 `--tick`(13.5 s)+`--once`(25 s 硬超时) ⇒ 那条**从来就没跑完过**(被掐)⇒ 别拿它的缺失当"机制没装好"。
- 🔴🔴 **后台子进程一律无窗口(pythonw)**:`collabd.py --supervise`(投递常驻,每 10 s 一轮)+ 看门狗 `guard.py`(每 15 s 探活、子进程一死就重生)原本以 `python.exe`(**控制台子系统**)起来,被宿主/钩子/看门狗拉起时 Windows **新分配一个控制台窗口** ⇒ 每次重生 / 每轮 `netstat` 就闪一下黑窗(用户原话「一会弹出来一会弹出来的,影响我操作」)。🔴 **根因**:`guard.py Child.ensure()` 拉起子进程 `creationflags=0x00000008`(**只有 DETACHED_PROCESS,漏 CREATE_NO_WINDOW**);`wake-session.py` 的 `netstat` **没有任何 creationflags**。`collabd.py` 的 `netstat` 已于 09-29 修(带 `0x08000000`)。✅ **根治(2026-10-02 落地)**:所有常驻/后台 `subprocess` spawn 一律改用同目录 `pythonw.exe`(**GUI 子系统,Windows 永不为其分配控制台**)+ `creationflags` 补 `NO_WINDOW|DETACHED|NEW_PROCESS_GROUP`:① 三个脚本加 `_win_pythonw()` 解析器(模块级 `PYW`);② `collabd.py ensure_supervise` 与 `guard.py Child.ensure` 的 `sys.executable`→`PYW`;③ `wb-result-hook.py` 5 处 spawn(含 `_bg`/sweep/once/tick)`sys.executable`→`PYW`;④ `wake-session.py netstat` 补 `0x08000000`。⛔ **今后任何新加的常驻/后台 spawn 都走 `PYW` + NO_WINDOW**,⛔ 别再用 `sys.executable` 起会长期存活的子进程(备份 `*.bak-flicker-20261002.py` ×4)。
---
## §2 第二段 · 多会话执行
> 这一段**可单独使用**(不装钩子与锁也能跑协作程序本体)。
**四条通道各走各的**
· **派活** ⇒ 自动化(**唯一能开新会话的通道**;⛔ 钩子做不到)。
· **收结果** ⇒ 直读宿主库(0 token)。
· **机械判定** ⇒ 下沉到本地只读程序(`collabd.py` / `board.py` / `goalctl.py`)。
· **人看的** ⇒ 只有一个看板(`assets/board.html` + `board_ext.py`)。
**铁律(全部实测得来)**
- ⚠️ **【2026-10-03 口径已改 · 本条「四类」部分作废】** 现行=**两类**(① 主会话 ② 执行会话;唤醒会话/跟进会话/队列上报**整套退役**)⇒ 以**文首口径块**为准,⛔ **别照本条去建会话**。(原文保留仅为留痕,⛔ 不删。)
- 🔴 **同工作区 = 四类会话**(2026-10-01 用户口径,⛔ **已于 2026-10-03 作废**):**① 主会话(只管目标和方向)② 执行会话 ③ 唤醒会话 ④ 队列上报的跟进会话**。四类**同处一个目录**,**靠标题两级前缀区分**(第 1 级=角色/第 2 级=任务类别),⛔ **不按 `cwd`**。✅ 第 ④ 类**解析层 + 投递路由都已落地** —— 投递目标=**跟进会话**(`follow_for_topic()`),解析不出 ⇒ **喊用户**,⛔ **不降级投主会话**(用户:「**换新会话 唤醒的是 跟进会话,主会话只能是用户触发**」)。🔴🔴 **第④类的职责只有一件事:创建执行会话**(用户 2026-10-01 22:5x 细化,逐字:「**跟进会话 只负责 ,创建协作会话(1、跟进上报后判断是否创建 2、被唤醒后 跟进目标情况 判断是否创建)**」)⇒ 两条触发、同一个动作:**收到队列上报** 或 **被唤醒** ⇒ 跟进目标情况 ⇒ **判断是否建一条 `[协作]` 会话**。⛔ **它自己不做具体活**(⛔ 不改台账 `state`、⛔ 不写 `blocked.json`、⛔ 不派活、⛔ 不抢锁)—— 那些是**它建出来的那条执行会话**的事。细则 ⇒ `architecture.md §2.3.0c`。⚠️ **看板图上这三处写的是简称**(用户 2026-10-01 23:1x 定):**唤醒**=唤醒会话、**跟进**=队列上报的跟进会话、**协作**=执行检查 —— 简称**只是压缩版面**,⛔ **不是又多了角色**(尤其「**协作**」⛔ 别读成「**协作会话**」,那是另一层)。对照表 ⇒ `architecture.md §2.3.0c-2` / `collab-detail.md`。
- 🔴 **`board.html` 的"虚线大框"=分组,⛔ 不是节点、也⛔ 不是"新一层"**(用户 2026-10-01 第四改:「用一个**虚线大框**把 主会话 唤醒会话 和 跟进会话都框起来,这个虚线大框 **连接 协作会话 虚线大框**就好」)⇒ `UA`=主会话+唤醒+跟进、`UB`=执行会话那一排,两组之间**只有一条线 = ① 派活**(⛔ 不再从主会话往每一格画射线)。⚠️ 两个框的尺寸**都从里面的格子现推**(⛔ 不写死坐标)—— 改了格宽不重算框 ⇒ 虚线**横穿文字**,而**那种图照样能渲染**(两道自检守着它)。细则 ⇒ `architecture.md §2.3.0c-2`。
- 🔴🔴 **开工第 0 步 = 会话规则机制体检**(用户 2026-10-02 明令)⇒ **先查清,再动手**(补建会话是它**后面**一步)。用户给了**两句,第二句是纠正**:
· ① 原话逐字:「**这个会话和协作会话的技能包 运行的第一件事 ,就应该是检查清楚 所有会话规划是否配置完整且生效,然后标记一个状态**」
· ② 原话逐字:「**就应该是检查清楚 所有会话规则机制 是否配置完整且生效, 不是规划 是 规则**」⇒ 对象 = **规则机制**(钩子 / 闸门 / 技能指针 / 常驻 / 编排…),⛔ 不是"排期规划"。🔴 首版按①的字面做成「会话规划体检」、**只查排期那一面** ⇒ 当天实测出的三类失效(钩子注入指向**已退役技能名** / 快照**写进幽灵目录** / **每轮注入的记忆**里指针悬空)**一条都查不到** ⇒ 旧脚本已退役到 `<WS>/归档/技能包-旧件-20261002/`,**同包内只剩一个入口**(两个入口 = 「在册 ≠ 生效」本身)。
⇒ **怎么跑**:`"$PY" "<本包>/scripts/session-rules-check.py" [--ws <工作区>]` —— ✅ **已接进工作区 `state.py`**(跑状态快照就自带这一段,⛔ 不必另记一条命令)。
⇒ **查三类、十二项**:**A 机制装没装好** ① 关键钩子在册 ② 钩子脚本路径存在 ③ 钩子注入里引用的技能名**是否还存在** ④ 钩子**真在被调用**没(闸门日志新鲜度)|**B 规则载体同没同步** ⑤ **每轮注入的记忆**里引用的技能名存在 ⑥ 常驻规则快照**不比权威旧**|**C 编排在不在跑** ⑦ 唤醒 / 跟进两台**周期钟**(缺 = 没人推 / 没人收)(⚠️ 其中「唤醒」这台是**代偿形态** —— 定案的唤醒时钟=**常驻投递**(⑩ 那一项查的才是它);「唤醒排期在册」⛔ **不等于唤醒时钟已就位**,两件事要分开读)⑧ 排期绑的模型**会不会被服务端拒**(`model_is_thinking=0` + flash 系 ⇒ 每触发必拒,2026-10-02 实测)⑨ `cwds` **归属同形**(错一字面 ⇒ 裂组且自我强化)⑩ **投递(常驻)**心跳 ⑪ 三类会话**当前有没有活的** ⑫ 有没有「**从未运行**就失效」的一次性排期。
⇒ **标记**:结论写成 `<WS>/.workbuddy/collab/session-rules.json`(`verdict` = `ok`/`warn`/`fail` + 逐项 `detail`)—— 后续会话与看板**读它**,⛔ 不靠人复述。
🔴 **为什么必须是第一件事**:2026-10-02 实测——排期**都在册**、模型**都可用**、cwds **都同形**,**却三类会话一条活的都没有**(=配置在、机制没在跑);同一天还查出钩子注入文本指着**已合并退役的技能名**、常驻快照脚本**写到没人读的幽灵目录** ⇒ 全是「**看着有配置、其实没生效**」。这类状态**不问就不会知道**,等它表现成"卡住"时已经晚了。
🔴 **判据本身也要能报出问题**:⑨ / ⑫ 这两项(以及 `cwds` 判据的边界)用**合成样本 + 四个变异体**做过红绿对照(夹具 `<WS>/tmp/rules-check-mutate.py`,跑完即弃)—— 变异体=判据恒空 / 判据放宽成"同父目录即报" / 把"从未运行"当"跑完了" / 不排除"还有下次触发"的排期,**逐一按预期报红**。⛔ **别拿"实跑一次没报错"当验收** —— 判据恒空时那次实跑**同样是绿的**。
⛔ **只标记、不设卡**:体检 `rc≠0` 也照常开工 —— 它的职责是**把状态问清楚**,不是拦人。
- 🔴🔴 **开工清单:主会话开工的第 0 步不是"派活",是"把三类会话摆好"**(用户 2026-10-01 明令:「**开始会话完成需求的时候,主会话需要创建 唤醒会话 以及根据分工类别 创建 协作会话 和 跟进会话呢 不然整个机制跑不起来**」)⇒ 建 **唤醒会话** `[唤醒]-<类别>-<具体>`(少建=**没人推**,需求原地静着)+ **每个分工类别一条执行会话** `[协作]-<类别>-<具体>`(少建=**没人干**)+ **跟进会话** `[跟进]-<具体>`(少建=**没人收**)。🔴 **跟进会话是全局唯一席位、⛔ 不按类别各建一条**(2026-10-02 用户订正);它是**收口者**:执行会话干完活把**待核对状态**写进执行队列,上报给**这固定的一个**跟进会话处理。⚠️ **只有自动化能开新会话** ⇒ "建会话"=登记一条自动化,⛔ 不是自己 spawn;标题**第 2 级必须带方括号、值取 `goal.json` 的 `topics`**(⛔ 用 `short` ⇒ 静默漏管)。细则 ⇒ `architecture.md §2.3.0d`。
- 🔴🔴 **缺会话 ⇒ 自动拉起**(用户 **2026-10-02** 口径,逐字:「**是用户说 使用协作会话方式 完成目标 或 继续完成目标**」)
⇒ 用户说这两句(或队列堵住)时:**先查三类会话齐不齐、活不活**;**缺 ⇒ 机制自己补建排期把它拉起来**,
⛔ **不许把"你去开一条会话"甩给用户**(旧行为=写 `NEED-USER.md` 喊人开会话,本条**取代**它)。
· 判据 + 现成排期参数 ⇒ `collabd.py --gap [--json]`(**只读**:判缺 + 给 `automation_update` 的 name/prompt/scheduledAt)
· 把结论**送进会话** ⇒ 钩子 `wb-result-hook.py::maybe_inject_session_gap()`(`UserPromptSubmit` 注入,触发词命中即查)
· ⛔ **脚本不许写 `automations` 表**(双红线)⇒ 建排期只能由**会话**用 `automation_update` 执行
—— 这一步**就是"自动"的全部通路**(用户只需照常说那句话,什么都不用做)。
· 🔴 **跟进会话只有一条、不带类别**(2026-10-02 用户订正,逐字:「**跟进会话只创建一个,跟进的内容
来自 执行会话执行完成 后 把 待核对状态 写入 执行队列,上报给那个 固定的 跟进会话处理**」)
⇒ `collabd.py --gap` 对 `follow` **不按类别分桶**(桶键恒 `(follow, "")`,显示成「全类别(固定席位)」),
拉起的排期名=`[跟进]-队列上报(固定席位·不分类别)`;`worker`/`waker` **仍按类别**分。
细则 ⇒ `architecture.md §2.3.0g`。
- 🔴 **「接续会话」是「形态」,⛔ 不是第 5 类**(用户 2026-10-01 原话:「**接续会话 不是单独的一类会话,是这几类会话到达阈值时 创建的接续会话**」)⇒ **角色继承被接续的那条**(主会话的接续仍是主会话候选)。
- 🔴 **执行与投递「一直运行」(常驻)** —— 09-29 定案,**2026-10-01 用户再确认**(理由:「**可能不是所有队列都是钩子产生的**」)。
⛔ **不要用自动任务当闹钟**(用户 2026-10-01:「**定时任务的方案已经废弃了**」);宿主钩子只作**补充**,⛔ 不是主路径。
- **一棒一线**;**派活 ≠ 结束**,要建监管棒并跟进。
- **自动化四律**:开机第 0 步跑状态 | prompt ⛔ 不抄任务细节 | **下一棒 id 只来自工具返回值** | 排期=收口+3~4 分钟、每条线只挂一个。
- **`cwds` 逐字同形**(去重键=`path.trim().toLowerCase()`);**入口头部声明的「工作区」决定归属**。
- 🔴 **⛔ 别用 OS 文件锁做并发**:本机实测「隔离目录全绿、上生产即 `rc=124` 卡死」⇒ 用**临时文件 + `os.replace` 原子替换 + 回读核对(最多 3 次)**。
- 🔴 **⛔ 不在「会干活的会话」里起常驻后台任务**:它会**压制该会话的 idle 钩子**(实测被僵尸任务压 6h20m);
要常驻 ⇒ 用**专用容器会话** + `stdout` **全重定向**(⛔ 否则输出反复唤醒宿主 ⇒ 界面静默哑掉)。
- 🔴🔴 **2026-10-02 23:0x 实测订正(⛔ 推翻当天早些时候的错误结论,本条为准)**:
一句判据:**不是「本机不存在长跑进程」,是「载体不同」—— 从工具调用进程树里起的活不过当轮,宿主后台任务/用户自己从桌面起的活能长跑。**
① **实测坐实**:同一时刻用 `start /b` 与 `Popen+DETACHED` 各起一条每秒打点的探针 ⇒ 两条**活到约 11 分钟后停在同一 tick**(67/64 行)⇒ **差别不在起法关键字,在父链**。
② **反证(⛔ 就是它让我误判的)**:MCN 工作台 `<mcn-short-video>/.../mcn-work-shop/start.bat` = `start "" node.exe server.js 8900`,用户**从桌面双击**、属用户登录会话 ⇒ **一直活着**;另有 4 个 `node.exe`(会话名 `Console`)长期存活。
③ ✅ **正解(本轮已跑通,两条都现算)**:走**WorkBuddy 自己的后台任务**(工具的 `run_in_background`,⛔ 别用 `subprocess` 自己造)⇒ 载体由宿主管理、不由当轮工具调用决定:
`python tmp/start_board.py 8788` → `board.py --serve --takeover` ⇒ **`127.0.0.1:8788 LISTENING`(pid 43176)/`HTTP=200`/119 704 B**;
`python tmp/start_mcn_board.py 8900` → **`LISTENING`(pid 14056)/`HTTP=200`/1 797 B**。
⚠️ 一次**带 `--takeover`**(静默并存会看到旧图);⚠️ 用 `pythonw.exe` 起子进程(GUI 子系统 ⇒ Windows 永不分配控制台 ⇒ ⛔ 不闪窗);⚠️ `pythonw` 无 stdout ⇒ **显式重定向到 `tmp/board-serve.log`**,否则静默无痕、连"起没起"都查不到。
④ ⛔ **因此撤回**当天那条「改用排期当时钟」的建议 —— 它建立在错误前提上。**排期仍保留,但身份是复活兜底**,⛔ 不是时钟。唤醒时钟**回到常驻进程本身**。
⑤ ✅ **S8 那套自愈仍有效**(`--supervise` 心跳 + `--tick` 顺手续命 + 存活判据 `pid 活 ∧ 心跳新鲜(<90 s)`)—— ⛔ 但它**不是**因为"没法长跑"才需要,而是**常驻总会被各种事打断**(重启、换会话、用户手动收),所以要能自动补回来。
🔴 **判"常驻在不在"只看两样**:`pid 活 ∧ 心跳新鲜(<90 s)`(`logs/supervise-heartbeat.json`);
⛔ **不许拿 `_tick.stamp`/投递日志当证据** —— 那些轮次是**钩子**写的(`pitfalls.md P0-22`)。
🔴 **启完必查三样**(⛔ 别凭"打印了启动消息"当成了):`netstat` 里有 **`LISTENING`**(⛔ `TIME_WAIT`/`FIN_WAIT_2` 是历史连接残留,不算)+ `curl` 有 `200` + `tasklist` 里进程在。
- 🔴 **自动唤醒任务的排期名必须带 `[执行]`**,否则被认成"主会话"把通知投给自己。
⚠️ 🔴 **2026-10-02 标注(⛔ 不是口径变更)**:该规则**仅代偿形态适用**(定案时钟=常驻投递,不开会话);🔴 **定案口径**=「协作与投递**一直运行(常驻)**」+「**定时任务的方案已废弃**」(2026-10-01 用户原话)。
- 🔴 **看板 tab 可并列查看别的「工作区」的目标**(用户 2026-10-03 报障,逐字:「**为什么 会话协作看板 tab 选项不能切换看另外两个工作区的目标**」)。
· **真因(看代码,不猜)**:`board.py::goal_files()` 只扫 `INBOX/goal.json` + `INBOX/goals/*.json`,而 `INBOX` 由**部署配置的 `workspace`** 决定 ⇒ **一个 `--serve` 实例天生只看见一个工作区**。
· **修法**:部署配置加 `peer_workspaces`(要并列查看的其它工作区,**正斜杠**)⇒ 把对方的 `goal.json` 读进来当**额外一格 tab**(打 `peer` 标记,前端显示在标题旁)。
· 🔴 **严格只读**:⛔ 不写对方文件、⛔ 不起对方进程、⛔ 不改对方状态;**活跃目标仍然只有本工作区那份** ⇒ `collabd.py` 行为零改动。
· 🔴🔴 **那一格的数据源必须跟着格走**(⛔ 否则=**假数据**,见 `pitfalls.md` **P0-40**):peer 格只喂 `_peer_tasks()`/`_peer_srows()`/`_peer_state()`(**只读对方目录**),读不到 ⇒ **空 + 界面如实说明**,⛔ **绝不拿本工作区的台账/会话/状态补位**(2026-10-03 实测:三格 `labor`/`sessions` md5 完全相同 ⇒ 把本区执行情况挂到了别人名下)。⚠️ 会话还要**单独补捞**(`_session_rows()` 只取全库最近 50 条,对方会话不在里面)+ 把 `sc["workspace"]` 换成对方根(`_sessions()` 有 `cwd` 硬过滤)。
· ⚠️ **它只是「只读概览」,⛔ 不是对方的完整看板**:peer 格的 `sessions` 目前为空(卡在 `in_project()` 判据,⛔ 不改那条 —— 它与 `collabd.py` 有**逐条同款**约束,历史已踩三次)。
· 🔴🔴 **2026-10-03 16:3x 用户定案:⛔ 不再为每个工作区各起一个看板** —— **看板只保留一份**(就是主工作区这一个),其它工作区靠 `peer_workspaces` 并列查看 ⇒ 各区的目标、验收、台账、常驻心跳**都在这份上看**(实测:三区常驻同时在跑,各自的 `heartbeat_age_min` 都能在这份看板上读到)。
· ⛔ 配置里**不含本工作区**(它已是 active 那格,重复列 ⇒ 出现两格);⛔ 去重键必须带**工作区前缀**(同名目标会互相顶掉)。
· ✅ 现算:`/board.json` 的 `goals`=**3 格**,`peer`=`会话协作测试1`/`会话协作测试2`(⛔ 区名是历史名,不改),`active` 恰好 **1** 个。
· ⚠️ 改 `board.py`/配置 ⇒ **必须重启**(**P0-38**),且用 `--takeover`(⛔ 否则新旧实例静默并存、同一个 URL 随机应答)。
- 🔴🔴 **自测的「现网真读数」类判据 ⛔ 不许复用 `imp()` 注入的测试环境**(2026-10-03 实测栽的,同族第 2 次):`imp()` 会把 `COLLABD_CONFIG`/`DSH_COLLAB_WS` 指到 `tmp/selftest/` ⇒ 用例里**无论怎么加载 `board.py` 都只读到测试那 1 格**(同一时刻命令行直接加载是 3 格)⇒ **恒绿与恒红都是假象**。正解三条:**加载前换 env、用完 `finally` 还原** + **在真磁盘上造目标**(`tempfile.mkdtemp()`)+ **变异对照**(打掉接线必须报红;还原后 md5 校回原值)。⚠️ 「配置缺省 ⇒ 回落旧行为」那条**只守配置侧**(代码被破坏时它照样绿)⇒ ⛔ 别把它当证伪项写进标题。详 ⇒ `references/pitfalls.md` **P0-39**。
- 🔴🔴 **每个工作区的执行检查由「它自己的主会话」起**(用户 2026-10-03 定案,逐字:「**每个工作区 会话协作机制的主会话自己创建后台任务 启动常驻协作程序**」,同句「**后台看板不用运行这么多 共享一份就可以**」)。
· **主会话开工第 0 步 = 确认本工作区的执行检查在跑**:判据=`.workbuddy/collab/logs/supervise-heartbeat.json` 里 **`pid` 活 ∧ 心跳距今 < 90 秒**;在跑 ⇒ **跳过**(⛔ 别起第二个,同端口两个实例会互相顶掉);不在 ⇒ 用**后台任务**启动一次,**输出必须重定向到文件**(⛔ 否则它的日志会把会话日志顶满)。
· ⛔ **不是由别的会话代起** —— 谁的工作区谁负责:跨区代起会让「哪个进程属于哪个区」彻底糊涂,而且**代起方一结束,被代起的那个区立刻失联**(实测就是这么断的)。
· 🔴🔴 **本工作区的会话 ⛔ 绝不许去起「别的区」的常驻**(用户 2026-10-03 16:4x 当场纠正,逐字:「**不是 为什么这个会话 要创建别的会话的 常驻任务,让那边的会话自己创建啊**」)—— ⚠️ **我自己就违反了这一条**:写完禁令后转头在本会话里替两个测试工作区起了后台任务(看着是"帮忙让它跑起来",实际是**代起**),当场被纠正后已停掉。
⇒ **正确做法**:那边的常驻**只能由那边的会话起**;我这边唯一能做的是**把它那条排期的触发时间提前**(改 `scheduledAt`,或等它周期触发),⛔ **不是替它执行**。
⇒ **怎么判断有没有越界**:问一句「**这个进程归哪个工作区?起它的会话又归哪个工作区?**」—— 两个答案不一致 ⇒ 就是代起。
· 🔴 **看板只保留一份**:各工作区配置里**没有** `board_port`;只有主工作区起一个 `board.py --serve`,靠 `peer_workspaces` 并列看其它区。⛔ 每区一个看板=白白多 N 个进程+N 个端口。
· 🔴🔴 **实测警告(决定这个口径能不能落地)**:**后台任务的寿命 ≈ 发起它的那个会话的寿命** —— 2026-10-03 实测两条:由**一次性会话**启动的那份只活 **5 分钟**(14:42:00 起 → 14:47:10 最后心跳,`round=32`);由**长期存在的会话**启动的那份已连续运行 **4.5 小时**(12:03 起仍在跑,`round=1624`)。⇒ 所以主会话必须是**周期性**的(现配 `FREQ=HOURLY;INTERVAL=1`)才能反复补,⛔ **别指望"起一次就一直活着"**。
· ⚠️ 由此推出的**残留**:仅靠周期性主会话 ⇒ 常驻每小时只在主会话那几分钟在线 ⇒ 要**真正长期在线**还得有一层**脱离会话**的载体(计划任务/桌面启动/系统服务)⇒ **尚未定,见本节末待定项**。
- 🔴🔴 **各工作区的「程序」也独立 —— 看板是唯一共用的一份**(用户 2026-10-03 17:0x 定案,逐字:「**跨工作区使用会话协作技能,除了看板共用,其余都是独立的,包括程序和相关文件**」)。
· **形态**:技能目录 `scripts/collabd.py` = **源(唯一真身)**;每个工作区 `.workbuddy/collab/collabd.py` = **它自己的一份副本**(另含 `goalctl.py`)。⛔ 各区**不再跑技能目录那份**。
· **一键分发**:`python scripts/deploy_code.py --ws <工作区>` —— 覆盖前**自动备份**、打印源与副本的 md5;支持 `--dry-run` 与 `--only collabd.py`。
· 🔴 **代价(明说)**:一处改动要分发 N 处 ⇒ **改完代码必须重跑分发**;⛔ 漏了**不报错**,只是"某个区行为不对",最难查。
· ✅ **可观测性(关键)**:`collabd.py` 每次写心跳都带 **`argv0`**(启动入口绝对路径)⇒ 「这个区跑的到底是哪份文件」一眼可查,**也能证伪**"改了副本却没重启"。
⚠️ **改副本不重启=没生效**(P0-17 同族)⇒ 主会话开工第 0 步要核 `argv0`:指向的不是本区路径就**自己换掉**。
· ⛔ **`board.py` 不进副本清单** —— 看板是**共用一份**的(用户同句定案);⛔ 每区一个看板=白白多 N 个进程与端口。
---
## §3 目录与依赖
```
SKILL.md install.py roots.env(装后生成) install.log(装后生成)
references/ architecture collab collab-detail rules deploy pitfalls taskgraph forensics manifest
scripts/hooks/ 6 份宿主钩子(10 条接线)
scripts/lock/ handoff-guard.sh preflight-lock.sh handoff-status.py op-lock.sh
scripts/ collabd.py board.py board_ext.py goalctl.py guard.py wake-session.py
stop-collab.py deliver-gateway-token.py selftest.py
collabd.config.example.json
scripts/forensics/ proc-parent.py(查进程父链 · 纯 ctypes · 只读)
assets/ board.html design-tokens.css
```
⚠️ 逐文件清单 + 来源(provenance)⇒ `references/manifest.md`(**唯一权威**,⛔ 别按本树猜)。
**依赖(⛔ 未并入,只声明)**:`agent-operating-rules` —— 跨项目「作业总规矩 / 说话方式」层,被大量模板引用,**留在原处**,本包只要求它同时可用。
---
## §4 验证(装完必跑)
```
python install.py --dry-run # 看 settings.json 将怎么变
python install.py --apply # 真装
python install.py --verify # 10 条接线空载荷 rc=0 + collabd --where + selftest PASS 39/0
python install.py --manifest --note "<本轮:…>" # 重算 references/manifest.md 的逐文件表(**改完包必跑**)
python install.py --uninstall # 还原 settings.json(应与装前备份逐字节相同)
```