- 变更规模:新增 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/ 知识文件,按口径入库)
329 lines
101 KiB
Markdown
329 lines
101 KiB
Markdown
---
|
||
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(应与装前备份逐字节相同)
|
||
```
|