Files
workbuddy_skills/session-mechanism/references/architecture.md
T

117 KiB
Raw Blame History

WorkBuddy 多会话执行 · 架构(唯一文档 · 在此迭代)

🔴 性质(2026-09-29 用户定案):本文件 =「多会话协作」的唯一架构文档。 · 改架构 ⇒ 只改本文件;⛔ 不另开平行文档、⛔ 不在别处再写一份架构。 · 与它冲突的旧件(交付物/多会话协同机制-定稿/…顶层设计/…实施方案/协同监管棒-SOP)一律降级为历史/过程:可作背景,⛔ 不作用判据。 · 本文件只讲**"该做成什么样"(架构);怎么操作(派活模板/作业规程/踩坑)⇒ SKILL.md 与 references/。 · 位置:~/.workbuddy/skills/session-mechanism/references/architecture.md(随技能走)。 · ⛔ 与「手机接入」那条业务线无关**(那是另一件事,见其自己的文档)。 🔴 2026-10-01 用户口径(最新):「接续会话 时间缩短 3-4 分钟即可」 ⇒ 排期基准 = 收口 + 3~4 分钟(原「5~8 分钟」作废)。


🔴 当前结论(先读本节 · 最后更新 2026-10-03 07:2x)

🔴🔴 2026-10-03 07:2x 口径改(本条为准,排在下面 10-01/10-02 那几条之上)——「上报/投递」整套退役

用户逐字:「没用了就删除,现在的机制是 协作程序接收和处理队列」; 又逐字驳回我把架构图上的「上报」改名成「常驻」:「怎么又把上报 改成常驻了 常驻什么,不是 协作程序常驻吗」。

一、投递这件事已经不存在了(⛔ 不是"降级",是代码真删): supervise() 里的 ①′待反馈序列 + ②③单条握手 + ④唤醒 四段 + 两个 _deliver_str() 调用点 203 行 → 71 行;--tick 里的 check_delivery_consumed() 调用删除;collabd.config.json 的 wake_enable true → false。⇒ 队列变化不再自动通知任何人;要落一件事,显式建一条任务会话。 ⇒ 删前实测证据:follow-retired 日志 1 313 行且每 20 s +1、notify_pending 四条全是 done、 wakeups.jsonl 文件不存在。⇒ 它是三重死锁(投递失败 ⇒ 不消费队列 ⇒ 队首永驻死件 S8=done ⇒ no_fb 恒 False ⇒ 唤醒条件②永不成立),⛔ 不是一条腿断。 ⇒ 现行机制只剩两条腿:接收=--report 写 tasks.json 四态台账(唯一权威); 处理=建 [检查]-… 排期(maybe_spawn_check_agent(),五道闸)。⛔ 没有"推送"这一环。 🔴 五道闸里的① 于 2026-10-04 收窄(用户口径:「检查程序必然要去处理队列」): sessions-ended(队列非空=有活没人干)不受目标状态限制; queue-empty 仍须「进行中」(它的职责是判目标该不该收口)。实体 ⇒ pitfalls.md P0-56。

二、⛔「常驻投递」这个角色名本身也不该再用 —— 那格的角色本来就叫「协作程序」(collabd.py); "常驻"只是它的一种跑法(--supervise),是运行形态、不是角色名。 ⚠️ 架构图上那两格(协作/上报)本来就是同一个进程,历史上真有两个进程才拆两格 ⇒ 退役后合并成一格「协作程序」,⛔ 不留"常驻"这个第二节点。 ✅ 判据(P0-37):「一个角色退役了」≠「承载它的那个进程也退役了」—— --supervise 的另一条职责「建检查会话排期」(maybe_spawn_check_agent(),唯一建 [检查]-… 排期处) 仍只有它承载 ⇒ 留进程、改副标题,⛔ 不删节点。

三、下文那些「常驻投递/唯一投递方/唤醒三条件」一律按此读: 「常驻投递」=已退役的职责,读到即跳过;--supervise 现存职责=① 建检查会话排期 ② 写心跳。 ⚠️ 「唤醒」那条整条随之失效(唤醒的载体就是投递)—— ⛔ 别再拿它当现行机制。 ⇒ 全文逐条改未做(散在 4 份约 200 处);⛔ 本块就是判据,与下文冲突处以本块为准。 ⇒ 机制级细节与验收清单 ⇒ SKILL.md 文首 10-03 状态块 + references/pitfalls.md P0-35 ~ P0-38。

⚠️ 为什么要这一节(用户 2026-09-30 点破):本文件此前是追加式的 —— 正文是旧的、最新结论沉在文末 §9, 谁先读正文谁就被带偏(我当天因此反复拿旧结论当现状)。⇒ 故设本节:一切以本节为准; 与下文冲突时,以「本节 + §9 最新一条」为准,并请顺手把冲突处就地标注。

项 当前结论
运行形态 🔴🔴 2026-10-03 07:2x 起「投递」整套退役(见本节上方状态块)—— 目标检查一直运行(常驻),但职责只剩「建检查会话排期 + 写心跳」。⛔ 「协作与投递一直运行」这句话里的"投递"半边已作废,只剩"目标检查一直运行"。起法见 §5-1(⛔ §5-1 里凡讲投递的段落同样按上方状态块读)。
投递 ⛔🔴 2026-10-03 07:2x 已退役,不是"降级"而是真删:supervise() 203→71 行、两个 _deliver_str() 调用点删除、--tick 的 check_delivery_consumed() 调用删除、collabd.config.json 的 wake_enable true→false。⚠️ 保留的 4 个函数(_deliver_str/follow_for_topic/wake_round/check_delivery_consumed)是零调用点实现,selftest 有 4 处真调用前两个 ⇒ ⛔ 删函数体=NameError;⚠️ 判"函数体内没调用 X"必须走 AST(selftest.py::_calls_in()),字面 grep 会被退役 docstring 永久假红(P0-36)。⛔ 「唯一投递方=常驻投递进程」整条作废。
接收/处理(现行两条腿) 接收=--report 写 tasks.json(四态台账,唯一权威,唯一真源);--once =纯投影,⛔ 不写。
处理=maybe_spawn_check_agent() 建 [检查]-… 排期(五道闸),由常驻 --supervise 每 2 轮判一次 —— ⛔ 这是这条腿的唯一载体(检查会话只能靠排期开)。
⛔ 没有第三环:队列变化不再自动通知任何人。
⛔ 已废弃 「用自动任务当闹钟」(2026-10-01 用户明确废弃,原话:「定时任务的方案已经废弃了」)⇒ §4.0/§4.1 只作历史。|🔴 2026-10-03 新增:队列投递段 + 唤醒回路(载体就是投递)—— 见本节上方状态块。
本机起法 🔴🔴 2026-10-02 23:0x 实测订正:不是"本机不存在长跑进程",是"载体不同"。
① 同一时刻用 start /b 与 Popen+DETACHED 各起一条每秒打点的探针 ⇒ 两条活到约 11 分钟后停在同一 tick(67/64 行)⇒ 差别不在起法关键字,在父链(从工具调用进程树里起的活不过当轮)。
② 反证:MCN 工作台 mcn-work-shop/start.bat 由用户从桌面双击、属用户登录会话 ⇒ 一直活着。
③ ✅ 正解=走 WorkBuddy 自己的后台任务(run_in_background;⛔ 别用 subprocess 自己造)。⚠️ 子进程用 pythonw.exe(GUI 子系统 ⇒ ⛔ 不闪窗)|⚠️ pythonw 无 stdout ⇒ 必须显式重定向到文件。
④ S8 自愈仍保留(--supervise 心跳 + --tick 顺手续命 + 判据 pid 活 ∧ 心跳新鲜(<90 s)),⛔ 但理由已换:常驻总会被打断 ⇒ 要能自动补回来。⛔ 别拿 _tick.stamp/投递日志当证据(那些轮次是钩子写的 —— P0-21)。
⑤ 🔴 启完必查三样:netstat 有 LISTENING(⛔ TIME_WAIT/FIN_WAIT_2 不算)+ curl 得 200 + tasklist 进程在。
⑥ 🔴 改看板要不要重启有四类答案(P0-38,⛔ 别一律重启):改 assets/board.html 不用(board.py:1293 每请求 read_bytes())/改 board_ext.py 不用(EXT_CACHE 按 (mtime,size) 热重载)/改 board.py 自身必须/改 collabd.config.json 必须(C = _cfg() 在 board.py:81 模块级执行一次)⇒ 判据=先答"这个文件是谁在读、什么时候读"。
看板服务(单实例) 🔴 2026-09-30 加护栏:Windows 的 SO_REUSEADDR 允许同端口重复绑定且不报错 ⇒ 多个 board.py --serve 静默并存、同一 URL 被随机应答 ⇒ 快照/代码版本打架。现:检测到已有看板 ⇒ 拒绝启动;换新代码 ⇒ --takeover。停机 ⇒ stop-collab.py。🔴 2026-10-03:/board.json 的 meta.deliver_retired == "2026-10-03" = 前端把「队列通知」卡画成退役说明的判据。
唤醒 ⛔🔴 2026-10-03 07:2x:这条整条失效 —— 唤醒的载体就是投递,投递已真删 ⇒ 所谓「三条件合取」(主会话未在处理 ∧ 无待反馈 ∧ 需求未完成)不再有执行者。⚠️ 下方 §2.0.1 那条「未读验收判据 ⇒ 误判完成」的缺口随本条一起冻结(判据本身还在,⛔ 但没人跑了)。⚠️ 「常驻=唤醒时钟」这个类比 ⛔ 别再沿用。
同工作区 + 会话类别 🔴 默认形态 = 一个工作区(2026-10-01 用户定)。🔴🔴 2026-10-03 起会话只两类:① 主会话 ② 任务会话(唤醒会话/跟进会话/队列上报已整套退役)—— ⛔ 原文写的"四类"作废。两类同工作区,⛔ 不靠 cwd。⚠️ 「接续会话」=形态,⛔ 不是第三类(撞阈值时由它自己建出来、角色继承被接续那条)。
已退役 交付物/ 里 4 份"多会话协同"平行件 + 落地清单-唤醒回路(2026-09-30 标注)|🔴 2026-10-03:队列投递段 + 唤醒回路(本节上方状态块有实测读数与三重死锁分析)。

0 任务目标(整套机制的运行中心)

🔴 机制的一切判定都围绕目标:判「需求是否完成」看它、判「接下来该派谁」看它、唤醒与摘要都引用它。 ⇒ 启动时必须有目标:guard.py 找不到目标(<inbox>/goal.json 缺 title)⇒ 拒绝启动(rc=3),并给出两种声明方式。 🔴 宁可不开,也不空转 —— 没有目标,机制根本不知道自己在为什么跑。

字段 作用
title 必需:一句话目标(摘要/看板/唤醒都显示它)
acceptance_doc + acceptance 验收判据(如 V1–V7)与其文档 ⇒ 判「完成」的依据
taskgraph + lines 任务图、涉及的线

声明 / 改写:guard.py --goal "<一句话任务目标>"(或直接编辑 <inbox>/goal.json)。

0.1 🔴🔴 目标不是固定配置 —— 它在「调用本技能的那次对话」里说明(2026-10-01 用户订正)

用户原话:「目标是 通过对话在调用 会话协作skill时说明的,不是固定的」

为什么单列一条:2026-09-30 本技能曾把 goal.json.topics 按工作区里现有的接续入口/接续包 文件名填了 4 类,并把它当成"待用户拍板定死"的事项。那是猜,不是说明。 🔴 危害不是"填错了会报错",而是 —— 它会静默决定「哪些会话算本项目、投递往哪条主会话去」, 且没有任何一处会说"这批类别是猜的"(同族红线:§2.3.2 那种"不崩溃,只是少说一句话")。

规矩(三条,全部可判)

# 规矩 落点
1 目标 / 为什么 / 任务类别 / 验收判据四样,都由"调用本技能时的对话"产生;⛔ 技能侧与脚本不许预设,⛔ 不许从目录名、文件名、接续入口名去推 本文件 + references/collab-detail.md §0.05
2 "说明"有唯一落点:goalctl.py declare --title … [--why …] [--topics …] [--kpi …] --yes(⛔ 默认干跑;--title 必填,脚本不替你编目标;省略 ⇒ 不动那一项) 使用方 .workbuddy/collab/goalctl.py
3 可覆盖:下次在对话里再说一遍 ⇒ declare 覆盖即可;⛔ 别把上次说明的当成项目常量 同上

未说明时 ⛔ 不许静默:topics 缺省 ⇒ 回落 goal.short(单类别,兼容), 但看板必须显示来源:project.topics_source.kind ∈ {declared, fallback, none} (fallback ⇒ 明写「⚠ 未声明 ⇒ 暂回落目标简称」)。✅ 已加回归用例。


1 五个主体(架构里只有这 5 个)

# 主体 只做什么 ⛔ 不做什么
1 用户 看一个看板;只拍"边界外"的板(含授权类:放行锁/放行重启) ⛔ 不管过程、⛔ 不必催进度
2 主会话 判断 + 派活:读库+看板判缺口 ⇒ 写一行排期 ⛔ 不替别线干活;⛔ 不做机械判定;⛔ 不搬砖(除非最靠前那步自己就能做)
3 任务会话 一棒一线、一次一件:做本棒,做完即上报 ⛔ 不跨线;⛔ 不夹带;⛔ 不常驻
4 目标检查 持有需求台账;接收上报;判定/告警/体检/看板/单例 🔴 ⛔ 不派活(不写排期、不开会话)
5 投递 逐条读队列 ⇒ 有新变化/静默 ⇒ 告诉主会话(投递方 = 唯一推进队列方);机械判定 ⛔ 不开会话、⛔ 不派活、⛔ 不落盘/不回显口令(口令只进程内用)

宿主(WorkBuddy 本体)=地基,⛔ 不是第 6 个主体。它只提供四样:状态(三张只读表)/调度(自动化排期=唯一能开新会话的通道)/事件(钩子)/互斥(域锁)。

🔴 职责硬边界(2026-09-29 用户定案):「投递(通知主会话)」唯一属于投递 —— 目标检查只维护队列/台账与投影,⛔ 不做任何投递。 · 理由:两条路径各自判重 ⇒ 同一内容成对重复投递(实测:同 hash 秒级两次)⇒ 每次投递都往主会话会话队列里压一条 ⇒ 压满即"卡死假象"(22:53 那次事故的根因)。 · 🔴 2026-09-30 更新:常驻程序的投影轮(--once)必须传 mutate=False,⛔ 不许消费队列(否则"消费掉却不投递" ⇒ 通知永远发不出去)。详见 §4.2。 ⚠️ 同批写的「不再靠常驻/投递改由宿主钩子完成」已于 2026-10-01 被用户推翻(定案:执行与投递一直运行,见 §5-1)。

🔴 谁是主会话/谁是任务会话 ⇒ 靠「会话命名前缀」区分([主] / [协作] 开头)—— 同工作区、跨工作区都适用,⛔ 与 cwd 无关(见 §2.3)。


2 主线:需求台账(状态一律上报,⛔ 不靠猜)

① 任务会话**开始执行** ──上报──▶ 目标检查:该需求置「执行中」
② 任务会话**处理完毕** ──上报──▶ 目标检查:该需求置「已完成」
   (被挡住 ──上报──▶ 置「**有阻碍**」+原因 ⇒ 主会话**必须**向用户喊话)
③ 投递**逐条读台账** ──有新变化──▶ **只反馈一条**给主会话(§2.1 握手)
                      └─(a)主会话未在处理 ∧(b)无待反馈 ∧(c)需求未完成──▶ **唤醒**(§2.2)
④ 主会话:核对产物 ⇒ 判缺口 ⇒ **派活**(写一行排期)⇒ 宿主到点拉起任务会话
⑤ **常驻的投递**逐条读台账 ──有新变化/静默──▶ 投递 + 推进队列(§5-1)
   (**常驻程序**同样**常驻**运行;宿主钩子只作**补充**,⛔ 不是主路径)
项 约定
需求台账(唯一权威 · 就存在这一个地方) tmp/supervise-inbox/tasks.json,由常驻程序持有。一条记录 = 一条需求项(状态 + 线 line + 执行者 + 产物 + 阻碍原因)
状态(四态) 待执行 pending → 执行中 running → 已完成 done,外加 有阻碍 blocked(2026-09-29 用户定案:需求状态必须有「有阻碍」这一态)
上报通道 python collabd.py --report <需求id> --state pending|running|done|blocked [--line 线] [--by 会话名] [--artifact 产物] [--reason 阻碍原因](本地命令 · 零 token)
审计流 每次上报追加一行(append-only,⛔ 不参与判定)
通知 投递写 TO-MAIN.md + 用一种能到达主会话的通道投出去(现为网关 reply,不夺会话)
唤醒判据 🔴 三条件合取(主会话未在处理 ∧ 无待反馈 ∧ 需求未完成)⇒ 发唤醒 ⇒ 主会话核对需求推进状态。详见 §2.2(⛔ 不是计时器)

🔴 红线:任何"执行中/执行完毕/有阻碍"只能来自上报。 ⛔ 不得用 mtime/超时/文本解析等推测代替 —— 历史故障(僵尸认领、超时后队首复活、同线互斥失效)全部出自"猜"。

2.0.1 🔴 收口必同步任务图 —— 否则唤醒会永久发(★ 2026-09-30 实测定型)

现象:N9/N10 都已 done(台账已上报、产物已独立核对),但唤醒仍在发,且每次都判「需求仍未完成」。

真因:goals_open() 的判据是「台账 有非 done 条目 ∪ 任务图 有非 done 节点」—— 而 goals_open() 读任务图(交付物/任务图.json) 那份是静态人工件,棒只上报台账、不会去改任务图 ⇒ 任务图的 status 必然长期滞后 ⇒ 并集恒为真 ⇒ 唤醒永久发(变成噪音)。

为什么不能"把判据改成只认台账"(⚠️ 这是本条的关键取舍,⛔ 别顺手改): 并集是有意为之的防漏 —— 任务图里可以存在「已规划但还没派棒、因而不在台账里」的节点; 若只认台账 ⇒ 这类节点不会被唤醒提醒 ⇒ 漏待办(比噪音严重)。

✅ 正确处置 = 补"同步纪律",⛔ 不是改判据

  1. 主会话收口时(判需求完成前)必须同步任务图:把已 done 的节点在任务图里一并标 done ⇒ 两处状态一致 ⇒ goals_open() 自然归假 ⇒ 唤醒自然停(它本来就是这么设计的:三条件之一不再成立 ⇒ 不再发)。
  2. 棒上报 done 时只动台账(⛔ 不越界改任务图 —— 那是主会话的规划件)。
  3. ⚠️ 反之:主会话新增任务图节点后,应尽快派棒,否则该节点长期"已规划待派"⇒ 唤醒会持续提醒(这是期望行为,不是噪音)。
  4. ⚠️ 判 goals_open() 时两处都读(现状),读不到任一处 ⇒ ⛔ 不因它判完成(宁可多提醒,⛔ 不漏)。

「有阻碍」的四条规矩

  1. 必须带原因(--reason),写清卡在哪 + 谁在等;⛔ 不许只标"卡了"。
  2. 上报为 有阻碍 ⇒ 投递会单独反馈该条,并要求主会话明确向用户喊「需用户介入」。
  3. 解除由主会话上报(拿到用户放行后 ⇒ 上报回 待执行/执行中);解除时原因一并清掉(⛔ 不留过期原因误导判断)。
  4. 🔴 原先外挂的"受阻清单"文件并入本台账(退役)—— ⛔ 不许两处存状态(今天的故障之一正是外挂与队列各说一套)。
  5. ⚠️ 谁报「已完成」:由能核对产物的一方报(通常主会话;任务会话做完也只能报"我提交了",最终以产物核对为准,⛔ 不认自述)。 ⚠️ 唤醒不是一个定时自动化 —— 它是投递的一个静默判据(架构上不引入任何周期性排期)。  🔴 2026-10-02(S8)已治本 —— 以下标注保留作历史:常驻停机那段已修,时钟由 --tick 顺手  ensure_supervise() 续命(见 §5-1)。⇒ 宿主排期 [唤醒]-<类别>-<具体>(FREQ=HOURLY;INTERVAL=1)  仍保留,但它的身份降为「引擎全静默时的复活兜底」,⛔ 不再是时钟本体。  ⚠️ 与它并列容易被混的一条(🔴 2026-10-02 按用户口径收敛 —— 旧写法「任何会话都别挂后台任务」属过度泛化):  禁区是「用户会在上面发消息的那条会话(=主会话)」,⛔ 不是「会话后台任务」这个形态本身。  用户 2026-10-02 09:59 原话:「那是因为之前跟进就是跑在主会话的,用户发消息和跟进一起处理会卡,  所以才把跟进独立成一个会话专门处理」⇒ 真因=与用户的轮次抢占,⛔ 不是"挂在会话名下"。  ⇒ ① 主会话 ⛔ 不许挂长跑(一挂就挤掉用户的话,实测压成 parkInQueue「只进不出」);   ② 跟进会话独立之后,它自己挂长活载体 ⛔ 不再影响用户在主会话的操作 ⇒ 允许(形态见 §2.3.0e);   ③ ⚠️ 但载体优先级仍是「常驻程序 > 会话后台任务」 —— 后台任务有两个已知代价(§2.3.0e)。

2.3 🔴 「都在同一个工作区」——可以,且区分方式已定(2026-09-29 用户定案)

可以:派活时把排期的工作区都指向同一个即可。好处:会话不再按工作区裂组、台账/看板单一、路径不跨盘。

🔴 区分方式 =「两级会话命名前缀」 —— [角色]-<任务类别>-<具体>(例:[协作]-[手机接入]-N9 复测)—— 同工作区、跨工作区都适用,因为它与 cwd 无关。 · 第 1 级 = 角色(🔴 四类,2026-10-01 用户口径:[主]/[协作]/[唤醒]/[跟进];⚠️ [跟进]=口径已定、尚未落地,见 §2.3.0c)⇒ 程序据此判"谁是谁"(投递目标、握手、唤醒条件)。 · 第 2 级 = 任务类别(🆕 2026-09-30 用户定:[手机接入] 这类,取自 goal.json 的 topics 清单, ⛔ 缺省才回落 short)⇒ 一眼看出"在协作什么";同一工作区里多任务并存时按它归类。 · 🔴 "任务类别"是一个词,四处同义(⛔ 别当成四个东西): goal.topics 的一项 ≡ 会话标题里的 <任务类别> ≡ 台账条目的 line ≡ 分工板的一行。 · ⚠️ 缺第 2 级 ⇒ 判「未按约定命名」并告警(只写一级会混掉"谁 vs 在做什么");⚠️ 标题改不动时用 --declare --role main|worker --topic <类别> 补声明。 · ⭐ 只需在派活时给自动化命名加前缀:实测会话标题 = 自动化名 ⇒ 前缀自动带进标题,不用额外握手、不用改会话。 · 判定优先级:① 命名前缀 ② 显式声明(collabd.py --declare --role main|worker,sid 自动从环境取;留给"标题没前缀"的场合)③ 未声明 + 告警 ⇒ ⛔ 绝不回落到 cwd 推断。 · 类别归属写进台账条目(line 字段,上报时声明)⇒ ⛔ 不从会话 cwd 反推"它属于哪一类"。

2.3.0 🆕 同一工作区 · 多任务类别(2026-09-30 用户要求,已落地)

用户原话:「之前的协作会话是跨工作区的,要能支持同一个工作区 多会话协作 (主会话根据任务自动梳理任务类别:通过协作会话名称前缀的方式区分具体执行会话, 所有主会话,协作会话,自动唤醒任务,都在一个工作区)」

为什么旧版不够:旧版归属判据只认一个 goal.short ⇒ 同一工作区里跑两个任务类别时, 除那一个之外的类别会被判「不属于本项目」⇒ 看板不列它们、--ready-next 不算它们 ⇒ 静默漏管(不是报错,是"什么也不说")。分工板同理:"线"就是工作区名 ⇒ 同工作区多类全塌成一行。

落地后的规则(三条,全部可判)

# 规则 落点
1 类别清单 由 goal.json.topics 声明(⛔ 缺省回落 [short] ⇒ 单类别部署行为不变)。主会话维护,改完下一轮钩子即时生效(⛔ 不必重启) collabd._goal_topics() / board._goal_topics()
2 每个类别各有自己的主会话。解析:① 标题里出现该类别名(最长优先)② 同类别多条 ⇒ 显式 主控 优先、否则最近活动;默认类别(topics[0])取不到时回落 default(⛔ 只为兼容单类别) collabd.resolve_mains() / _scan_mains() / board._scan_ws_mains()
3 投递按件的类别选「跟进会话」(🔴 2026-10-01 第四改;follow_for_topic()):类别已登记 ⇒ 严格投该类别的跟进会话(取不到就记 no-follow-session + 喊用户,⛔ 不投给别类别);类别未登记(空/旧数据里的工作区名)⇒ 只有一条活的就用它、多条取默认类别那条(⛔ 不随机挑)。⛔ 绝不投主会话(用户:「主会话只能是用户触发」) collabd._deliver_str(..., topic=) ⇒ follow_for_topic()
4 解析出一条主会话以后,还要能认出"换人了"(登记失效 ⇒ 按工作区解析 + 显式告警) collabd.resolve_main() ⇒ 注意:它现在只服务看板与"谁是主会话"的读数,⛔ 不再是投递目标

命名(🔴 都在一个工作区,靠前缀分)

  • 执行棒(含自动唤醒任务):[协作]-<任务类别>-<具体>
  • 主会话:主控 · <任务类别> · <具体>(也可写 [主控]-[类别]-<具体>)
  • ⛔ 自动唤醒任务的排期名也必须带类别前缀(⚠️ 本规则只在「唤醒用排期」这个代偿形态下才有对象 —— 定案的时钟是常驻投递、不开会话、也就没有排期名;⛔ 别因为这条规则在,就反过来认定「唤醒本该用排期」)—— 否则它自己会被解析成"没有类别", 且活动一多就会被当成主会话候选(实测踩到:[协作]-唤醒A · <工作区> 缺第 2 级)。
  • 🆕 唤醒主会话(2026-10-01 起 · 第三类角色):[唤醒]-<类别>-<具体> ⇒ parse_session_name() 返回 role='waker'(原只 main/worker)。 🔴 它自己的三条定性(用户 2026-10-01):「唤醒会话 是独立会话,不要在主会话上处理, 是随着需求确定时创建的」⇒ (a) 独立一条会话,⛔ 不是主会话的职能;(b) ⛔ 叫醒的活 不在主会话上处理(执行体是它自己);(c) 随需求确定时创建 ⇒ 看板上「还没建」是正常态。 🔴 它是一条会话(用户定性:「唤醒脉冲会话,本质还是会话」(现名:唤醒主会话))⇒ 判"主会话与任务会话 都空闲"时必须把它排除;不排除 ⇒ 它用自己的存在否掉条件 ⇒ 自指死结(醒一次、白判一次、接着睡)。 ⚠️ 两道排除都要:① CODEBUDDY_SESSION_ID(精确,程序跑在会话内时="自己") ② 名字前缀 [唤醒](程序在会话外跑时拿不到前者,只能认名字)。 实测教训:现役会话名写作 主控 · …(中点分隔、无方括号)⇒ 解析不出角色 ⇒ 只靠名字会漏。 详见 references/collab-detail.md §1.4a 与 §0.5.5。

已知边界(诚实标注)

  • ⚠️ 类别名是子串匹配(主会话是口语式命名,没有方括号)⇒ 类别名别取太通用的词(如"机制"), 否则会误吞别人的标题。源码里已做"最长优先",但根本解法是类别名取得足够具体。
  • ⚠️ 台账里的历史条目 line 仍是工作区名(跨工作区时代的产物)⇒ 见 §2.3.2: 它们不是类别(kind='legacy'),但不许静默消失(折叠成「未归类」格照画 + 图外点名 + 表内标注)。 ⛔ 不迁移历史数据(迁移=改写记录);新条目上报时用类别即可。

2.3.0b 🔴 默认=一个工作区 + 「接续会话」是第四种命名形态(2026-10-01 用户定 · 已落地)

用户原话:「将多会话协作机制 改为默认 在一个工作区下运行(主会话(所在工作区)、协作会话、唤醒会话), 支持会话创建接续会话的情况下 也能正常运行」

① 默认形态 = 一个工作区(不是"可支持的选项",是默认)

项 定论
哪些会话在同一个工作区 全部会话:主会话、任务会话、唤醒会话(+第④类「跟进会话」,未落地),以及它们各自到阈值时产生的「接续」形态 —— 都在 collabd.config.json 的 workspace 那一个目录下跑。🔴 「接续」不是第 5 类(用户 2026-10-01 订正)—— 它继承被接续那条的角色
区分方式 只看会话标题(两级前缀)—— ⛔ 不按 cwd 分线、⛔ 不按工作区裂组、⛔ 不回落 cwd 推断
配置层已清理 删掉 collabd.config.json 的 lines(键=工作区名 ai1net-dsh-desktop/ai1net_ui,跨工作区时代的产物,与 goal.topics 打架)⇒ 分工维度只有任务类别一处(goal.json.topics)
⛔ 别复活 ⛔ 不要再加"按工作区分线/按工作区选主会话"的配置 —— 那正是 09-29 之前的老形态

② 「接续会话」=由会话自己建出来的下一棒,判据是它自己的命名形态**(continuation)**

🔴 2026-10-01 用户订正 + 实测缺口(先读这条) · 定性:接续不是一类会话,是"这几类会话到达阈值时创建的接续会话" ⇒ 角色继承被接续的那条。 ⛔ 所以不许把它当第 5 类,也⛔ 不许给它固定角色。 · 🔴 现状缺口(实测,2026-10-01 19:5x):parse_session_name() 把凡是标题含「接续」的一律判 role="worker"(form="continuation"),而 _scan_mains() 的排除判据是「角色不是 worker/waker」 ⇒ 主会话的接续被当成干活的棒排掉。实测本工作区 8 条会话(接续 · 会话机制合并包 · 任务4b-4d-6、 接续 · 会话机制合并技能包(…)、[接续] …、接续棒:…、[唤醒机制] 接续 · … 等)全部判 worker ⇒ _scan_mains().cand = 0 ⇒ resolve_main() 返回 {"sid": "", "source": "no-register"} ⇒ 投递解析不出主会话(no-main-session)。 · ⚠️ 为什么不能简单地"把接续都判角色未知":那就重演自指死结 —— [唤醒机制] 接续 · … 这类 第一对方括号里装的是类别,判成"角色未知"后它又会被收成主会话候选(原事故:通知投给它自己)。 · 🔴 根因是信息不足,⛔ 不是解析器不够聪明:continuation 形态的标题里根本没有角色信息 ⇒ 解析器无从继承。⇒ 正解只有两条,待拍板(见 §2.3.0b ③ 与 pitfalls.md P0-11)。

它为什么是个真缺口(2026-10-01 实测坐实,⛔ 不是理论问题): 接续会话是自动化起的新会话,标题由建它的那条会话写 ⇒ 常写成 [唤醒机制] 接续 · 日志事前叫停钩子落地(第 2 棒) / 接续棒:日志增长治理(任务 0 → 任务 A) —— 没有角色方括号。而当时 _scan_mains() 只排 [协作] ⇒ 这类棒:

  1. 进了主会话候选;
  2. 又因为它标题里有 [<类别>] ⇒ _topic_in_title() 认出类别 ⇒ 直接被解析成"该类别的主会话" ⇒ 投递把通知投给这条棒自己(自己叫自己、白判一次),真主会话被架空。

判据(四种形态 · 唯一实现处 + 它的镜像)

form 标题形态 role ok
prefix [角色]-[类别]-<具体>(合规) main/worker/waker ✅ True
prefix 只有一级 [协作]N9-…(缺 [类别]) worker ❌ False
🆕 continuation 接续会话:[<类别>] 接续 · … / 接续棒:… worker ❌ False
🆕 main-prefix 主控前缀:主控 · <类别> · …(中点分隔、无方括号) main ❌ False
  • 🔴 ok=False 恒表示"没按两级前缀约定命名"(语义未变 ⇒ 老调用方行为不变); 🆕 新增的是 role 的兜底判定 —— 形态不合规但角色明确时照样给出 role。
  • 🔴 主会话候选的排除判据:从「标题不含 [协作]」改成「角色不是 worker/waker」 ⇒ 一处改动同时覆盖 [协作]-…、[唤醒]-…、接续会话三类(⛔ 少一类就会重演上面的自指死结)。
  • 实现处(⛔ 两处必须同款,改一处必须改两处):collabd.py::parse_session_name()(权威)/ board.py::_role_of_title()(镜像)。⇒ 已加真对账用例 selftest.py::命名:看板与常驻程序**同一套角色判据**: 它把两边的函数拉出来逐样本比对(⛔ 不再靠人盯)。
  • ⚠️ 已知边界(诚实标注):[<类别>] <具体> 不带"接续"二字的(如 [手机接入] 复测) ⇒ 仍判角色未知(⛔ 不猜它是执行棒 —— 归类要靠 goal.topics 才能判,而解析函数不读配置)。 这类会话会被看板点名(sessions_unrecognized),⛔ 不静默消失。

③ 给"建接续会话的一方"的硬规矩(⛔ 派活模板要带上) 建接续自己的下一棒时,标题必须按形态写 —— 否则新会话要么冒充主会话、要么在看板上归不了类别:

执行棒 / 接续棒: [协作]-[<类别>]-<具体>
主会话的接续:  主控 · <类别> · <具体>
唤醒:         [唤醒]-[类别]-<具体>

🔴 上面这两条规范其实早就写着了 —— 问题出在"没被执行":主会话的接续只要标题带 主控, parse_session_name() 就判 role="main"(✅ 已实测:主控 · 机制线(…) ⇒ main/main-prefix)。 ⇒ 本缺口的两条正解(⛔ 两条都还没做,属方案变更,等拍板):

  1. 补登记(零代码改动,最小):把主会话线的 接续 · … 会话用 collabd.py --declare --role main 登记 ⇒ 走 resolve_main() 第①层。✅ 立刻可用;⚠️ 登记是静态的,换主会话后不会自己变(§7.1 那次事故的成因)。
  2. 改判据 + 收紧命名(治本):continuation 形态不再无条件判 worker —— 标题带 主控 ⇒ main(现在已经如此); 否则 ⇒ 角色未知(ok=False + 落 sessions_unrecognized 点名)。⚠️ 必须同时堵住自指死结: [<类别>] 接续 · … 的第一对方括号装的是类别,判"角色未知"后它会重新被收成主会话候选 ⇒ 需要一条显式排除("缺角色方括号 ⇒ 不进候选,只点名")。⇒ 这条改的是排除判据本身,故不擅自动。

2.3.0c 🔴 第④类会话 =「队列上报的跟进会话」(用户 2026-10-01 口径 · ✅ 解析层已落地)

用户原话:「这里的第4类应该是 队列上报的跟进会话(之前考虑只让主会话管目标和方向, 队列上报的会话 让专门的 跟进会话处理)」

它要解决什么:主会话现在既管方向、又逐条跟进队列上报(执行中/已完成/有阻碍)——两件事混在一条会话里 ⇒ 主会话被琐碎跟进占住、方向判断被稀释。⇒ 拆出一个专门的跟进会话承接队列上报的处置。

🔴🔴 2026-10-01 22:5x 职责收敛(用户细化口径,⛔ 推翻本节早先"由它处置队列上报"那句)

用户原话:「跟进会话 只负责 ,创建协作会话(1、跟进上报后判断是否创建 2、被唤醒后 跟进目标情况 判断是否创建)」

⇒ 它只做一件事:判断「要不要创建协作会话」,要就建一条。 两条触发、同一个动作:

# 触发 它做什么
① 收到队列上报(目标检查投递进来) 跟进目标情况 ⇒ 判断是否创建任务会话
② 被唤醒(宿主排期定时拉起)
🔴 2026-10-02 标注(⛔ 不是口径变更):定案=常驻投递按周期把它叫醒(见 §5-1);现在由排期顶上,因为常驻停机
同上(这一条不依赖推,是它的主通道)

⛔ 它自己不做具体活:⛔ 不改 tasks.json 的 state、⛔ 不写 blocked.json、⛔ 不派活、⛔ 不抢域锁 —— 那些是它建出来的那条任务会话的职责。⚠️ 只有自动化能开新会话 ⇒ "建会话"=登记一条自动化。 ⚠️ 已同步到生产排期 [跟进]-会话协作自检-队列上报(2026-10-01 22:5x · nextRunAt 未变=23:21:35)。

✅ 已落地(2026-10-01 20:4x · 实测 [跟进]-[会话协作自检]-… ⇒ follow) —— 四处同批改齐:

# 落点 改动
① collabd.py::parse_session_name() 角色表加 "跟进": "follow"(+ 形态表那行同步)
② board.py::_role_of_title() 同款镜像
③ 两边的主会话候选排除元组 ("worker","waker") → ("worker","waker","follow")
④ selftest.py 逐样本对账加 [跟进]-…(3 项 → 4 项)+ 两处字面断言同步

🔴 为什么这四处必须同批改:[跟进]-… 若没进角色表 ⇒ role 解析为空 ⇒ 会被 _scan_mains() 收进主会话候选(标题里带 [<类别>] 时还会被解析成「该类别的主会话」)⇒ 通知投给它自己 —— 与接续会话同款的自指死结(见 is_continuation() 那段)。

🔴🔴 同批修掉的一个"一直存在但看不见"的缺陷:board.py 原来把会话角色二分 (登记/解析出的主会话 ⇒「主会话」,其余一律「协作会话」)⇒ 唤醒会话与第④类跟进会话都被标成「协作会话」 ⇒ board.html 第三层(按 role 挑格子)会把它们误画进"任务会话"那一排: 唤醒会话画两遍(主会话左侧一次 + 这排一次)、跟进会话顶着错名字。 它一直看不见,是因为这两类会话从来还没被真的建出来过 —— 建出来的那一刻就会显形。 修法:新增 board.py::_role_label() 输出四类,board.html 那排判据同步改成 role === '任务会话' (⛔ 从 role !== '主会话' 改过来 —— 两者必须同款,改一边不改另一边 = 又开始吃错格子)。

✅ 投递路由 —— 2026-10-01 落地(⛔ 本节早先写的"降级投主会话"已被推翻)

用户原话(本轮口径):「换新会话 唤醒的是 跟进会话,主会话只能是用户触发」

⇒ _deliver_str() 的目标从"该任务类别的主会话"改成"该任务类别的跟进会话":

场景 记什么 同时做什么
类别已登记 且该类别有跟进会话、活着、不哑 正常投(记 target:"follow") ——
类别已登记但没有该类别的跟进会话 skipped=no-follow-session 喊用户(NEED-USER.md)
找到了但都不在活会话里 skipped=follow-not-live 喊用户
目标正 working skipped=target-busy 等它空闲(⛔ 不降级,见 §4)
目标已哑(诊断日志撞 ~10 MiB) skipped=target-deaf 喊用户换会话

🔴🔴 为什么"降级投主会话"被推翻(⛔ 不许改回去):主会话是用户与系统交互的那一面 —— 自动化往它推消息=用机制噪音挤掉用户的话(实测过:会话被压成 parkInQueue「只进不出」, 用户随后发的消息排在后面,表现为"卡住、没反应")。⇒ 解析不出 ⇒ 喊用户,⛔ 不静默、⛔ 不盲投。 ⚠️ 配套两处必须同批(改一处=判据漂移):_main_sid() 加 wake.target == "follow" 的防污染判 (否则"上次投给谁"会把跟进会话回读成主会话)、以及看板第三层的格子判据。

2.3.0c-2 🔴 看板的"分组大框"(2026-10-01 用户第四改)

用户原话:「先等等 有个办法更好,用一个虚线大框把 主会话 唤醒会话 和 跟进会话都框起来, 这个虚线大框 连接 协作会话 虚线大框就好」

图上现在有两个虚线分组大框(.grp,虚线、不填色),⛔ 不是"新加了两层":

框 套住什么 为什么是它俩
UA(上) 主会话 + 唤醒会话 + 跟进会话 机制侧的三个角色本来就在同一组里:用户只进主会话;机制侧的"推"只叫醒/投递给跟进会话
UB(下) 任务会话那一排(个数是活的) 它是干活的那一组,与上面那组是派活 → 干活的关系

两组之间只有一条线 = ① 派活(取代了原来"主会话往每一格射线"的扇形)。

🔴🔴 2026-10-01 23:1x 图上三处标签改「简称」(用户第六条口径)

用户原话:「唤醒会话 改为 唤醒,跟进会话 改为 跟进 字体和 唤醒一样大小\n协作程序 改为 协作」

格 原名(正文全称) 图上简称 字号
RW 左格 唤醒会话 唤醒 n-title-sm(13px)
RW 右格 队列上报的跟进会话 跟进 n-title-sm(13px)—— 用户明令与"唤醒"一样大小
PB 常驻程序 常驻程序 协作 n-title(16px,沿用原样)

⚠️ 简称只是"图上写不下"的压缩,⛔ 不是又多了两个角色:正文/文档/代码里的全称不变 (唤醒会话/队列上报的跟进会话/目标检查)。⇒ 图里 ? 与 aria-label 必须带简称对照, 否则看图的人(含下一次的自己)会把「协作」误读成「协作会话」—— 那是另一层、另一回事。 ⚠️ 给简称做探针要小心:唤醒 是 [唤醒]-…、唤醒机制 等一堆串的子串 ⇒ 拿它当"这词还在不在"的判据 ⇒ 必假红(本轮渲染桩就因此假红一次,已改成查被删那段的内容 + 加防恒真对照)。

🔴 这一改顺手消掉两个麻烦(⛔ 不是巧合,是分组本身带来的): ① ① 派活不必再穿过唤醒/跟进那一行的空档去找格子(只剩两个大框之间 58px 一段); ② 唤醒 → 跟进那条实线再也不与任何竖线十字相交(用户上一句要的"实线"落到实处)。 ⚠️ 改里面任何一格的宽度(TW/FW/RW_GAP)都必须重算框(UAx/UAw 由三格包围盒现推; UB 由该排布局现推)—— 否则虚线横穿格子上的文字,而那种图照样能渲染(=没人守就没人发现)。 ⇒ 两道自检守着它:tmp/arch-geom-check.mjs ⑦ 段、tmp/render-check.mjs 的分组框断言。

2.3.0d 🔴🔴 主会话"开工清单":开工时必须把三类会话建齐(用户 2026-10-01 明令)

用户原话:「开始会话完成需求的时候,主会话需要创建 唤醒会话 以及根据分工类别 创建 协作会话 和 跟进会话呢 不然整个机制跑不起来」

一句判据:主会话开工的第 0 步不是"派活",是"把三类会话摆好" —— ⛔ 少建哪一类,机制就断在哪一节:

该建什么 标题形态 少建会怎样(实测后果)
唤醒会话 [唤醒]-<类别>-<具体> 没人推动:主会话与任务会话空闲时没有任何"叫醒"入口 ⇒ 需求停在原地(本轮实测:需求 3.5 小时零成果、triggers[].state='none')
任务会话(每个分工类别一条) [协作]-<类别>-<具体> 没人干活:resolve_mains() 按类别找不到承接者 ⇒ 投递降级成"无主会话";类别形同虚设
跟进会话 [跟进]-<类别>-<具体> 没人收上报:队列上报(执行中/已完成/有阻碍)全挤回主会话 ⇒ 主会话被琐碎跟进占住(正是 §2.3.0c 要解决的那件事)

三条硬约束(⛔ 踩了就白建)

  1. 只有自动化能开新会话(宿主钩子开不了)⇒ 主会话建会话 = 登记一条自动化,⛔ 不是自己 spawn。
  2. 标题必须两级前缀 [角色]-[类别]-<具体>,且第 2 级带方括号、值取 goal.json 的 topics。 ⛔ 用 goal.short(简称)⇒ parse_session_name() 认不回 ⇒ 会话静默漏管(§2.3.2 表 ④ 就是这么踩的)。
  3. "还没建"是正常态、⛔ 不是失败 —— 唤醒会话按定性随需求确定时才创建。但一旦开工就必须建, 且看板上 state:'none' 要如实显示成灰 ○「还没建」,⛔ 不许美化。

怎么验:建完在两处独立读数对上 —— sessions 表里三条标题的 role 非空(waker/worker/follow) + 看板第三层只出现 [协作] 那条(唤醒/跟进⛔ 不进这排)。

⚠️ 过 §6 判据:这条要求同时存在 3 类会话,而 §6 那条写着「必须存在的会话数=0」 ⇒ 有真取舍(多发几条会话 vs 主会话注意力):取舍结论=接受,因为三条各自对应一个断点(没人推/没人干/没人收), ⛔ 不是"多发几条好看"。⛔ 但**不许拿"这是用户说的"跳过判据复核。

2.3.2 🔴🔴 "读到了却没说" —— 本机制最容易犯的一类 bug(2026-09-30 收尾棒各抓到一个)

形状:不崩溃,只是少说一句话。所以"跑一遍没报错 / build 通过 / 自测全绿"永远验不出来 —— 只能靠两处独立读数对账。下面逐条记(⛔ 别删旧条 —— 每条都是同一形状的新伪装):

# 事故 坏在哪 修法(防回潮的用例)
① acceptance_state 只有说明行时被读成"全过" 状态判据写的是 if not acc(判字典空不空);实际有 _说明/_更新 两条说明行 ⇒ 条件永不触发 ⇒ 控制台打「三路全过」 判据改成「有没有有效项」(键不以 _ 开头)⇒ goal_state() 三态:open/pass/undeclared(一条有效判据都没有 ⇒ ⛔ 不算过)。board.py 的命令行摘要同族(… or "无" 读起来就是"全过")一并收敛到 _acc_summary()。用例 selftest.py::看板:验收摘要行
② 台账里的旧线在图上"整块不见" 类别清单已迁走,台账里仍有跨工作区时代的旧线(各 2 件且都已完成)。旧版把它们也当"分工位"排进 labor(6 行),图上 slice(0,4) 只画前 4 格 —— 恰好全是"件 0"的类别 ⇒ 4 件已完成的活一格都看不见;图外只说「另有 2 个未画」不点名 _labor() 每行加 kind(topic/legacy)⇒ build() 出 orphan 汇总(条数+件数求和)⇒ 看板把 legacy 折叠成一格「未归类」照样画(⚠ 不属任何当前任务类别 + 线名 + 件数)+ 图外点名+ 台账表标注 (旧值 · 不在当前任务类别清单)。MAXW 4→5。用例 selftest.py::看板:『未归类』不许被静默丢掉
③ 看板一页只能看一个目标;tab 第一行只给简称** ⇒ 认不出是哪个目标** 用户 2026-10-01:「把协作实时看板改为 tab 支持多个目标执行协作状态展示」+「再确认下目标名称显示在哪里的,没看到呢」。根因两条:① 快照只出单份 goal(顶层 goal/project/tasks/labor… 全是活跃目标那一份)⇒ 别的目标根本读不到;② tab 第一行用 short(简称=「本机协作」),全名只藏在 title= 悬停提示里 —— 又一例「有却不说」 ① board.py:加 GOALS_DIR/goal_files()(goal.json + goals/*.json,活跃排第一)+ _goal_block();build() 出 goals[],顶层键=活跃那一份(⇒ ⛔ 老渲染器与老断言逐字照旧可用);台账按 line ∈ 目标 topics 切,不属任何目标的件归活跃目标且照常显示;_scan_ws_mains/_resolve_main/project_scope/_sessions 全部加可选 topics/g/rows(宿主库只读一次,N 个目标共用)。② board.html:renderTabs()/paintScope()/scopeView(),当前格记进 URL hash #g=<目标id>(刷新/分享都不丢);切格只重画不取数;tab 第一行改完整目标名。用例 selftest.py::看板:多目标 tab(11 项)+ tmp/render-check.mjs(双目标样本 tmp/board-verify-2goals.json:改 hash 再重画,验"切格真换数据",⛔ 不是只数 tab 格数)

| ④ | 真派出去了、也真在跑的棒,看板上"整条消失" | 用户 2026-10-01:「为什么创建了协作会话 但是 看板中没有展示」。真事:M5 那根棒被命名成 [协作]-本机协作-M5 … —— 第 2 级没带方括号,而且用的是 goal.short(本机协作)而非任务类别(topics=唤醒机制)。⇒ parse_session_name() 判 ok=False、in_project() 也找不到 [唤醒机制] ⇒ 它不进 sessions、不进任务会话层、也不算"本项目会话" ⇒ 静默消失。🔴 根因在规范本身:collabd.py 生成的 NEXT.md 原文写着「主题取自 goal.json 的 short」—— 按这句话命名出来的棒,判据必然认不回(_in_project 收的是 topics,且要方括号) | ① 规范纠偏:那句话改成 [协作]-[<类别>]-<具体>,第 2 级带方括号、值取 goal.json 的 topics,并把"写错 ⇒ 静默漏管"写在同句里(collabd.py::_next_md)。② 说出口:_sessions() 多收一路 unrecognized(同工作区 cwd 尾名相同、但没通过三级归属判据的会话)⇒ build() 出 sessions_unrecognized ⇒ 看板在图外说明里点名(⚠️ 在跑/近 3h 的列名字,更旧的只计数 —— 兼顾"精简"与"不许少说一句")。③ ⛔ 绝不改归属判据:它们不进 mine、不算「本项目会话共 N 个」;cwd 只用来决定"要不要提醒"(_ct == _wstail),⛔ 绝不用来决定"归不归本项目"(守 §2.3「绝不回落到 cwd 推断」)。用例 selftest.py::看板:同工作区但没归入本项目的会话不许静默丢(4 项)+ tmp/render-check.mjs 那一条(灌一份带 sessions_unrecognized 的快照再画,验"说出口 + 不改 N") |

| ⑤ | 归属判据只认"带方括号",而在跑的任务会话标题恰恰没带 ⇒ 看板第三层画空框「暂无协作会话」,而那条会话正在 working | 用户 2026-10-01:「看板没有正常显示协作会话」。逐条读数:[协作]-机制排查与修复-常驻投递容器 —— 第 2 级没带方括号 ⇒ 两处归属判据(board.in_project()/collabd._in_project())都判 False ⇒ 它落进 sessions_unrecognized、不进 mine ⇒ 第三层(按 role==='任务会话' 挑格)一条都挑不到。🔴 而 _topic_in_title() 的注释白纸黑字写着「判据是子串(不要求 [...] 包裹)…两种写法都要认」⇒ 同一个标题,一个函数说命中、另一个说不命中(判据打架)。🔴 第二个受害者更严重:ready_next() 的判据①(除主会话外没有别的会话在 working)走的就是 _in_project() ⇒ 在跑的执行棒判 False ⇒ 它落进 ignored_others 而非 working_others ⇒ --ready-next 会说"可以排下一棒" = 用户 2026-09-30 描述的那个病(「前面还没执行完 就开始下一棒了」) | 两条判据统一成三条并列(任一命中即算):① 主会话 sid ② 角色可解析 + 标题出现任一类别(子串,⛔ 不要求方括号)③ 老写法 [<类别>] 保留。⚠️ ② 里的"角色前缀闸"是精度保障(正文里恰好提到类别名的无关会话解析不出角色 ⇒ 仍不算)。用例 selftest.py::归属:两处 in_project 判据逐样本对账(11 例逐条比布尔 + 精度反例 + 两处源码判据形态) | | ⑥ | 接续棒冒充「主会话」(同一处判据的第三处漂移) | 2026-10-01 扫线上标题时发现(⛔ 不是推理):collabd.is_continuation() 判据是 "接续" in name(整串),而 board._role_of_title() 只查方括号后面那段 ⇒ [接续] 会话机制合并技能包(第 1 棒) 两处结论相反(worker vs 空)⇒ 看板的主会话候选排除元组拦不住它 ⇒ 「主会话」位上坐着一条 355 分钟没动的接续棒(= is_continuation() 注释里那个"自指死结"的同一族) | board._role_of_title() 判据改成整串:if "接续" in nm: return "worker"(逐字同款 is_continuation())。对账样本加 [接续] … 一条,并断言两边都判 worker(⛔ 判空就报红)。用例 selftest.py::命名:看板与目标检查同一套角色判据 |

🔴 判据(两条,可直接照做)

  1. 凡"取前 N 个 / 截断 / or 默认值" ⇒ 必须回答:被截掉的是什么、有几条、说不说得出名字。 "另有 N 个未画"这种提示信息量为零 ⇒ 等于没说。
  2. 凡"状态判定" ⇒ 判据要落在语义项上("有没有有效判据"),⛔ 不要落在容器形态上("字典空不空/列表长不长")—— 容器里放两条说明行,形态判据立刻就骗过自己。

验证场合:改完这类东西,必须走 references/collab-detail.md §0.5.4「改完看板怎么验」的三步(语法/渲染桩/几何), 并当场从产物里现取被测脚本(⛔ 不读上一轮导出的副本 —— 副本形态不同会造出稳定的假绿)。

2.3.3 🔴🔴 过期会话退场(治"接续棒越堆越多")+ 主会话"哑了要换一条"(2026-10-01 立 · 已落地)

用户原话:「通过接续会话的时候,如何处理过期的会话,避免越堆越多」

病:接续=会话自己建下一棒,而前棒不会自动消失 —— 它在宿主库里永远是一条 completed 记录、标题同族。实测本工作区已堆 5 条「接续 · 会话机制合并…」⇒ 看板与状态稿每轮都为旧棒占位, 真在跑的棒被埋在里面。

项 结论(已落地)
退场两条路径 ① 显式:collabd.py --retire self --by <接棒id> --why … ⇒ 立刻生效(接续协议的必做一步)
② 窗口兜底:completed 且超 session_live_min(默认 90)分钟未动 ⇒ 自动收起(前棒中途死掉时靠它)
两条闸门都绕开谁 working 永不收起(在跑的一定要摊在版面上)+ 主会话是版面锚点(⛔ 不按窗口抽掉)
存哪 tmp/supervise-inbox/retired.json({完整sid: {at,iso,by,why}})—— 原子替换 + 回读核对,⛔ 不上文件锁(本机锁接进生产必 rc=124,见 MEMORY.md)。⚠️ 表自身也过期(7 天)+ 上限 200 条
谁写谁读 写=collabd.py --retire(一个状态只有一个写者);读=collabd.py::fetch()(状态稿)+ board.py::_sessions()(看板)。⛔ board.py 只读不写
⛔ 退场 ≠ 删除 宿主库原样在、retired.json 全留痕(可逆、可追溯)⇒ 但必须报数:看板图外说明 + 状态稿都会写「已收起 N 条」
(⚠️ 收起而不说 ⇒ 读者把"收起了"读成"本来就没有"=同族红线)
渲染侧 board.html:从 d.sessions_retired 读,⛔ 一个字都不许自己判(判据两处打架是本项目最狠的一类坑)→ 说条数全量、点名最近 3 条、写清**"不是删除"、给出--retire self 那条命令**、窗口数值从载荷现取

主会话"哑了要换一条"(同轮修 · 这是 V2「主会话可响应」的硬阻塞真因):

  • 哑=会话诊断日志撞 ~10 MiB ⇒ 宿主拒写(EPERM)⇒ 界面永远刷不出内容。 🔴 它照样出现在"活会话"列表里 ⇒ 判据必须是「活着 ∧ 不哑」。
  • 缺陷①:resolve_main() 的第①条快路原为 if reg and (not live or reg in live) —— 只查活着、⛔ 不查哑; 而 ② 走的 _pick_live() 本来就排哑(2026-10-01 专为"别再投给哑会话"加的)⇒ 同一条哑会话「② 排得掉、① 排不掉」(判据打架,当日第三次同族)⇒ ① 也改成走 _pick_live()。
  • 缺陷②:--declare --role main 原语义是加不是换 ⇒ 而 _main_sid() 取 roles 里第一条 main, Python dict 对已存在的键赋值不改位置 ⇒ 老那条(已哑的)永远排前面 ⇒ 逃生口形同虚设。 🔴 掩盖得更深的一层:_deliver_str 的粗判不传活会话(resolve_mains(st, []))⇒ _pick_live 在 live 为空时回退 c[0] ⇒ 每轮先认下那条哑的、直接 target-deaf 返回, 走不到后面"拿锁后用活网关列表重算"那一步。 ⇒ 语义改成「声明自己为主会话 ⇒ 其它登记为 main 的一律降级为 worker」(一个工作区一条主会话) + 明确说出口。⚠️ 多类别"每类别一条主会话"走标题 主控 前缀(main_by_topic 解析),⛔ 不依赖这里。
  • 现场红绿对照(最强证据):改前 resolve_main → {"sid":"","source":"stale-roles"}、--tick → deliver=target-deaf; 改后 resolve_main → {"sid":"<活着的会话>","source":"roles:main"}、--tick → deliver=**target-busy** (主会话在跑 ⇒ 正确延后,⛔ 不是"投不出去")。
  • 病症本身的处置(runbook):把 <sid>.log(连同 .log.1)改名挪开(⛔ 不删,Python os.rename, ⛔ 别用 mv)⇒ 宿主数十秒内重建并恢复写入 ⇒ 详见 references/forensics.md §4。 ⚠️ 它必然复发(每个会话涨到上限都会重演)⇒ 靠扫描器 + 钩子兜底。
  • 用例:selftest.py::会话退场:过期会话收起但不删除…(13 项)+ selftest.py::主会话:登记为 main 但已哑 ⇒ ⛔ 不认它 + --declare main 是换不是加(6 项) + tmp/render-check.mjs 的 retBad(合成样本注射:判"说出口+说清不是删除+不计入", ⛔ 不能靠线上快照 —— 它是 0/0 时那条分支永不执行,见 pitfalls.md P0-13)。

同时必须满足两条(否则会踩坑)

  1. 文件与锁都不依赖 cwd:动别线文件用绝对路径;域锁按显式 domain 抢(domain 本来就是显式参数)⇒ 各线仓库虽不同,归属依然清楚。
  2. 提交边界按目录判断(⛔ 不按 cwd)。

2.3.0e 🔴🔴 跟进会话「长期活着」=默认形态(用户 2026-10-02 09:59 口径更正 · 本棒落地)

用户原话(逐字):「那是因为之前跟进就是跑在主会话的,用户发消息和跟进一起处理会卡, 所以才把跟进独立成一个会话专门处理」

① 这条口径更正了什么(⛔ 先读,别按旧结论干活)

  • 旧的 architecture.md(§2.3 那一行 + §4 那一条)把「⛔ 禁止把长跑服务放进会话后台任务」写成了普适禁令 —— 那是过度泛化。它的真实背景只有一条:当时"跟进"这件事跑在主会话上,与用户发消息抢占同一个轮次 ⇒ 会卡。 而当时的解法本来就是「把它独立成一条会话」。
  • ⇒ 跟进独立成会话之后,"它自己挂着长活载体"⛔ 不再影响用户在主会话的操作 ⇒ 上一棒「跟进会话也不能挂」的结论 作废 —— ⛔ 别再拿它当判据。
  • ⚠️ 禁令本身仍然有效,只是对象收窄:主会话(用户会在上面发消息的那条)⛔ 依然不许挂长跑。 判据是**"用户会不会在这条会话里发消息"**,⛔ 不是"它是不是会话后台任务"。

② 默认形态 = 跟进会话长期活着

  • 🔴 跟进会话的默认状态是「长期活着」,⛔ 不是"上报来了才临时起一条"。 理由:它同时是两条通道的落点 —— ① 队列上报的投递目标(follow_for_topic()) ② 周期唤醒的叫醒对象(§2.3.0c 表 ②)。它不在跑 ⇒ 投递与唤醒双双落空 (现场实测:名册里 6 条 [跟进] 都在、一条都不活 ⇒ 投递全落 follow-not-live)。
  • 🔴 载体优先级(从优到劣,⛔ 不许倒着选)
优先 载体 为什么
① 常驻程序(如 collabd.py --supervise,跑在会话之外) 不占会话、⛔ 不吃该会话的 idle 钩子、会话换了也不受影响
② 会话后台任务(挂在跟进会话自己名下) 合法且够用(依据见 §4.1.1 表 ①);但有两个已知代价,见下
  • ⚠️ 会话后台任务的两个已知代价(必须写进派活模板,⛔ 不许省)
    1. 🔴 该会话自己的 idle 钩子会被静默压制 —— 实测:该会话只要有 pending/running 的后台任务, 宿主的 idle 钩子就被无声掐死(有被僵尸任务压 6h20m、零日志的记录)。 ⇒ 后果:一切"钩子驱动的唤醒/监管"在这条会话上失效 —— 唤醒必须由常驻侧发,⛔ 不能指望它自己醒。
    2. 🔴🔴 载体不同,寿命完全不同(2026-10-02 23:0x 实测订正,⛔ 推翻当天早些时候"活不久"的结论) ⇒ ⛔ 从本会话的工具调用进程树里起的活(subprocess / start /b / start.bat)活不过当轮: 两条探针(start /b 与 Popen+DETACHED)在同一时刻一起停(tick 67/64)⇒ 与起法关键字无关,是父链。 ⇒ ✅ 能长跑的是这两种:① WorkBuddy 自己的后台任务(run_in_background,载体由宿主管)—— 实测 board.py --serve 8788 与 MCN server.js 8900 借它起,端口一直 LISTENING、curl 200; ② 用户从桌面双击起的(在用户登录会话里,如 MCN start.bat)。 ⇒ ⛔ 因此撤回"改用排期当时钟"那条建议(前提是错的):唤醒时钟回到常驻进程本身,排期仍是复活兜底。 ⇒ ⚠️ 后台任务照样会打断(重启/换会话/手动收)⇒ S8 自愈仍该留着。 ⇒ ✅ 顺带澄清一条已被推翻的旧担心:「任务挂在会话名下 ⇒ 卡消息输出」—— 不成立。 实测 mcn-short-video 的 :8900 后台任务跨会话收尾存活,用户照常发消息; 当日"卡"的真因是会话日志被推过 ~10 MiB 后丢写,与后台任务无关(§4.1.1 表 ①)。

③ 与常驻 --supervise 的分工(谁负责把跟进会话带回来)

谁 负责什么 ⛔ 不负责什么
常驻投递进程(--supervise;含 --tick 内的 ensure_supervise()) ① 提供唤醒时钟(周期叫醒跟进会话)② 把队列上报投递给跟进会话 ③ 保证常驻自己活着(掉了由下一次事件续命) ⛔ 开不了新会话(宿主硬边界:只有自动化能开会话)⇒ 它带不回一条已被回收的跟进会话
跟进会话(第④类) 长期活着;收到上报/被唤醒 ⇒ 只做一件事:判断要不要创建任务会话 ⛔ 不挂常驻程序本体(那是上格的活)
自动化排期([跟进]-<类别>-…) 唯一能把跟进会话"带回来"的通道 —— 会话被回收后靠它重新拉起 ⛔ 不是唤醒时钟(时钟由常驻提供)

🔴 一句判据:"谁把跟进会话带回来" = 自动化(只有它能开会话); "谁叫醒它、谁投给它" = 常驻投递。两者⛔ 不可互替 ⇒ 那条"带回来"的排期必须一直在册 (automations 里的 [跟进]-<类别>-…)。

⚠️ 诚实标注的缺口(⛔ 本棒未处置):automations 只新建不复用 ⇒ 现行"需要跟进就排期"的形态会每轮堆一条 (用户 2026-10-02 已就此抱怨)。曾列两条方向(取消独立跟进会话角色/允许目标检查直插 automations 行 —— 后者是双红线)⇒ 待用户拍板,⛔ 不擅自改。

2.3.0f 🔴🔴 投递目标的"候选收窄"必须发生在活会话过滤之后**(2026-10-02 S5 · 验收 V2 修毕)**

🔴 一句判据:"谁活着"是投递目标的唯一决定因素 —— ⛔ 不许在知道"谁活着"之前 先靠"最近活动"把候选收窄成一条,否则那一条死了 / 哑了 = 整个类别投不出去。

① 缺陷(同族两处,必须同批修)

  • ① 候选收窄过早:follow_for_topic() 旧实现先取 by_topic[类别](首见=最近活动那一条), 再交给 _pick_live() ⇒ _pick_live([死的那条], 活列表) 判"都不活" ⇒ sid="" ⇒ 同类别里明明还有活着的候选,也一辈子轮不到(现场实测:名册 6 条 [跟进] 全 completed ⇒ 投递连续落 follow-not-live)。
  • ② 终局短路错位:_deliver_str() 的粗判(live_sids=[],故意不做网关发现)拿它的 sid 反过来做终局判定 —— _tgt in _deaf_sids() ⇒ target-deaf + need_user,且在取锁之前 就 return。而 live 为空时 _pick_live() 按设计回退 c[0]("探测不到时不许假装知道谁活着") ⇒ c[0] = 最近活动那条,与"活着"无关 ⇒ 只要那条恰好哑了,每轮都短路, 永远走不到拿锁后那次"用活网关列表重算" ⇒ 判据自相矛盾(粗判说"哑、没人能收", 精确说"有活的"),而矛盾的结果是"不投"。 📊 实证:_collabd.log 里 target-deaf 共 81 次,末次 2026-10-01 21:07:53 (队列里 M5=done 被它压住);另 2026-10-02 10:33–10:41 实测粗判给 57f58ecf、 精确给空 —— 两条判据对同一批候选给出相反答案。

② 修法(两条,同批)

  • ① follow_for_topic() 的候选改成该类别候选全表(_scan_follows() 新增 by_topic_all, 按 last_activity_at 倒序)整体交给 _pick_live(): live 非空 ⇒ 挑"第一条活着且不哑的";live 为空 ⇒ 回退 cands[0] = 最近活动那条 ⇒ 与旧写法逐字等价(向后兼容不破)。⚠️ ⛔ 只在同类别内放开,绝不跨类别(那是"投错窗口")。
  • ② 粗判的"哑"判据改成只看候选集合、不看它的第一条:该类别候选"全都"哑才成立 "没人能收"(target-deaf);有候选活着 ⇒ ⛔ 不短路,交给精确阶段用活网关列表正挑。 候选集合为空 ⇒ 不判("一条都没有"由 no-follow-session 分档,⛔ 不重复告警)。

③ 取证

  • 钉死用例:selftest.py::t_coarse_deaf_not_terminal(5 项)—— 伪造"两条同类候选,最近活动那条哑、 另一条活"+伪造网关,真实 follow_for_topic() / _pick_live() / _deliver_str() 全程在位。
  • 回退反证:把 cand, src = _lst, … 换回 [_lst[0]] ⇒ 该用例 5 项全红、rc=1 (① 现 = target-deaf、② http=None、③ 投给空、④ 台账 0、⑤ 假警报 1 次)。
  • 回归:selftest.py 全绿 PASS 48 / FAIL 0(新增 1 例)。
  • 现场读数:修复并重启常驻后 _collabd.log 连续 follow-not-live(真值:确实没有活的 跟进会话),⛔ 不再出现 target-deaf 这一档。

⚠️ 剩余缺口(⛔ 本棒未处置,属环境):现场 6 条 [跟进] 全 completed ⇒ 投递与唤醒都到不了人(机制侧已能正确判定并喊用户)。补齐之道见 §2.3.0e ③: "把跟进会话带回来"只有自动化做得到 ⇒ 需在册一条 [跟进]-<类别>-… 排期。

⚠️ 操作提醒:改完 collabd.py 必须重启常驻 —— --supervise 是长驻进程,内存里是旧代码; 只有每次事件新起的 --tick 才会载入新代码(实测:改完后旧常驻仍报已被修掉的 no-follow-session, 新起的 --tick 报正确的 follow-not-live,两者在同一份日志里交替)。 ⚠️ 纯 CLI 调 --ensure 必须带 COLLABD_CONFIG(钩子会显式传),否则子进程"起后即退"并写 supervise.out.log 说"未找到部署配置,拒跑"。

2.3.0g 🔴🔴 缺会话 ⇒ 自动拉起(用户 2026-10-02 口径 · 本棒落地)

用户原话(逐字,分两次说全): ① 「应该是 用户说 协作会话 完成目标 和 继续执行 的时候 如果没有 相关会话就自动拉起」 ② 13:18 订正触发面:「是用户说 使用协作会话方式 完成目标 或 继续完成目标」

① 这条改了什么(⛔ 先读,别按旧结论干活)

  • 旧行为:_deliver_str() 判到「没人能收」(follow-not-live / no-follow-session / target-deaf) ⇒ 写 NEED-USER.md:"请把「X」那条跟进会话拉起来" ⇒ 把机制该干的活推给了用户。
  • 新行为:缺 ⇒ 机制自己补建排期把它拉起来。用户只需照常说那句话,⛔ 不要用户动手开会话。
  • ⛔ 它不推翻"不降级投主会话":主会话仍只由用户触发(§2.3.0c)—— 变的只是"缺了怎么办"。

② 为什么"自动"只能是这个形状(架构硬边界,⛔ 别去找第二条)

想让谁开会话 行不行 依据
脚本/常驻程序自己建排期 ⛔ 不行 写 automations 表对脚本是双红线(§2.3.0e ⚠️ 那段)
jobs/resume 拉起一条会话 ⛔ 不够 它给的是 kind:"background" worker,进不了 live ⇒ 接不到 sessions/{id}/reply(2026-10-02 复核)
宿主钩子直接开新会话 ⛔ 不行 宿主硬边界:只有自动化能开新会话
自动化排期 + 由会话建它 ✅ 唯一可行 排期是"把一条能收指令的会话带回来"的唯一通道(§2.3.0e ③)

⇒ "自动"= 钩子把缺口注入会话上下文 ⇒ 会话(零判断成本)用 automation_update 建排期。

③ 落地件(三处,⛔ 缺一处即"在册 ≠ 生效")

# 件 职责
① collabd.py --gap [--json](新) 只读判据:四类会话 × 每个任务类别 ⇒ state=ok/stale/none/unknown;hard=缺的跟进/协作;soft=缺的唤醒(用户定性「随需求确定时才创建」⇒ ⛔ 不算硬缺,否则红多必聋)。同时给出现成排期参数(name/prompt/scheduleType/scheduledAt/cwds)。
② hooks/wb-result-hook.py::maybe_inject_session_gap()(新) 在 UserPromptSubmit 把缺口注入当前会话(additionalContext,零 token)。触发词=「协作会话方式」/「完成目标」/(兜底)「继续执行」,命中即查、否则 120 s 节流;一切异常吞掉。
③ 规则载体 本文件 + SKILL.md §2 铁律 + collab.md §4.1b。

④ 两条判据纪律(⛔ 别打折)

  • 🔴 活会话探测不到 ⇒ unknown,⛔ 不报缺(与 _pick_live() 同一条规矩:探测不到时不许假装知道谁活着)。 否则 token 一缺就满屏假红 ⇒ 红多必聋。
  • 🔴 本命令只读:⛔ 不建排期、⛔ 不投递、⛔ 不改队列。建排期=会话的事(见 ②)。

⑤ 验收(本棒实测读数,2026-10-02 13:1x–13:2x)

  • collabd.py --gap 真跑:类别「会话协作自检」跟进 stale(在册 5、活 0)/协作 none(在册 0); 类别「机制排查与修复」跟进 stale(在册 1)、协作 stale(在册 5)⇒ 硬缺 4 条,唤醒软缺 2 条。
  • 钩子 --selftest 喂 {"hook_event_name":"UserPromptSubmit","prompt":"继续完成目标"} ⇒ 注入文本里出现 「⚠️【缺会话 ⇒ 自动拉起】…」+ 4 条带 name/scheduledAt 的排期建议(输出 971 B,含机械摘要)。
  • ⚠️ 诚实边界:"注入成功" ≠ "排期已建" —— 注入只把事推到会话面前,建排期那一步由会话执行, 故本条不能用"钩子跑了没报错"当验收(同 §2.3.2「读到了却没说」那族坑)。

⑥ 🔴🔴 跟进会话=全局唯一席位,⛔ 不按类别各建一条(2026-10-02 13:5x 用户订正 · 本棒改判据)

用户原话(逐字):「跟进会话只创建一个,跟进的内容来自 协作会话执行完成 后 把 待核对状态 写入 协作队列,上报给那个 固定的 跟进会话处理」

  • 错在哪(本棒初版就犯过):把齐备度一律按「角色 × 类别」展开 ⇒ 两个类别就报两条 [跟进] 硬缺 ⇒ 一次建出 2 条跟进排期。用户订正后:跟进是"收口者",不是"某类别的收口者"。
  • 改法(三处,缺一即回弹): ① GAP_GLOBAL_ROLES = ("follow",) —— follow 在 scan_roles() 里桶键恒为 (follow, ""), 不看标题里的类别;② session_gap() 对全局席位只出一个桶,⛔ 不按 topics 展开; ③ _gap_plan() 对全局席位出的排期名=[跟进]-队列上报(固定席位·不分类别),prompt 不绑类别。 worker(协作)/waker(唤醒)仍按类别分 —— 用户只订正了 follow 这一条。
  • 配套(任务会话的收尾动作):任务会话干完活 ⇒ 把待核对状态写进执行队列 ⇒ 由那一个固定 跟进会话去消费、核对产物并上报。⛔ 不是"再开一条跟进会话"。
  • 验收读数(改后真跑):[跟进会话] 全类别(固定席位):stale|在册 6、活 0 ⇒ 硬缺从 4 条降到 2 条 (=1 条跟进 + 1 条协作·机制排查与修复;协作·会话协作自检已 ok,在册 1、活 1)。

⑦ 闭环实测("注入 ⇒ 拉起"是不是真通了,2026-10-02 13:42–13:49)

  • 会话用 automation_update 建了一次性排期(scheduledAt=13:42)⇒ automation_runs 出现 IN_PROGRESS 行 ⇒ sessions 表里 15 秒后真的多出一条新会话(标题=排期名)。
  • ⚠️ 对照旧读数:此前 8 条 run 一律是 ACCEPTED 且 started_at/session_id 全为 None ⇒ 「ACCEPTED ≠ 跑起来了」;判"有没有真拉起"要看 IN_PROGRESS + sessions 里有新会话, ⛔ 别只看 run 有没有行。
  • ⇒ ⑥⑦ 合起来:缺口判据准了(跟进只 1 条)、拉起通路也真通了(排期 → 会话落地)。

2.3.1 🔴 同工作区下"会冲突"到底指什么、以及怎么解(2026-09-30 补 · 用户追问)

先说清:前缀那套解决的是「谁是谁」(投递目标/握手/唤醒),⛔ 它不解决「同时改同一个文件」。 ⇒ 同工作区的唯一真冲突面 = 文件互斥:主会话与任务会话共享同一批文件,可能同时改同一份。

定案 §2.3 已经给了答案:域锁按「显式 domain」抢,⛔ 不是按 cwd、⛔ 不是按工作区。 ⇒ 只要两个会话各报自己的 domain,域不重叠 ⇒ 真并行(这正是 §5 那句"域不重叠即真并行")。 ⇒ 实际做法:域按「文件 / 子目录」切,⛔ 不按工作区切。例(本项目的真实分工):

会话 它动的文件 该报的 domain
主会话 skills/session-mechanism/**(机制) 机制域
任务会话(N9/N10 棒) ai1net-dsh-anywhere/**、ai1net-dsh-desktop/**(业务) 各线域
⇒ 两者天然不重叠 ⇒ 同工作区也不会互相卡。

🔴 但实现上有两个真缺口(2026-09-30 实测,必须补)

  1. 没人真的去抢锁 —— 实测全盘无任何 .locks 目录;且当天真实发生过:collabd.py 被另一个会话并发改(我只能停手)。 ⇒ ⛔ 域锁"定义了"≠"在生效":它靠显式抢锁动作,谁不抢就没人拦。
  2. 派活模板漏写"抢锁" —— 我当晚派的 N9 棒 prompt 只写了「抢不到锁只报告」,⛔ 漏了"开工第 0 步先抢域锁" ⇒ 等于没接这条线。

✅ 硬规矩(派活模板必须逐字带上,⛔ 不许省)

开工第 0 步:抢域锁 —— preflight-lock.sh "<会话名>" <本棒要动的文件...>
  · 域按「显式 domain(文件/子目录)」报,⛔ 不按 cwd、⛔ 不按工作区;
  · 抢不到 ⇒ ⛔ 不硬上、⛔ 不删别人的锁、⛔ 不接管 ⇒ 只报告并停(写清被谁占、等谁);
  · 释放必带会话名;机制层锁须独占。

⚠️ 代价:任务会话会加载该工作区的项目指令(AGENTS.md / CODEBUDDY.md)⇒ 这个统一入口必须是"对的那一个"。

2.1 🔴 反馈协议:单条 + 握手(2026-09-29 用户定案)

投递一次只反馈一条任务状态,退回后进入等待状态;主会话执行结束才反馈下一条。

步 谁 动作
① 投递 发现一条新状态 ⇒ 只反馈这一条给主会话
② 投递 进等待状态:定期监督主会话是否在执行(判据 = 主会话的 sessions.status='working')
③ 主会话 处理该条(核对产物 ⇒ 判缺口 ⇒ 需要就写一行排期)
④ 投递 探测到主会话执行结束(working 消失)⇒ 才反馈下一条
  • ⛔ 不许一次倾倒多条(否则主会话被"口播十条"淹没);未反馈的按发生顺序排队。
  • ⛔ 等待期间不发唤醒 —— 一条没处理完,就不发第二条(⛔ 不叠加消息)。
  • ⛔ 兜底:反馈后 20 分钟未见主会话执行 ⇒ 放行下一条并记「需用户介入」(⛔ 不无限卡死整条链)。
  • 落地:collabd.py 的 supervise();"主会话"以最近一次投递到的那个会话为准。
  • 🔴 投递时机(2026-09-29 用户定案):投递前先判目标会话是否在跑 —— 只有「没在跑」才投。 正在 working 的会话,投进去会插进它当前的轮次里 ⇒ 等它空闲再投(与本节握手同源:握手管"顺序",这条管"时机")。 两道检查:① 声明的主会话在跑 ⇒ 直接延后(零网络,最省)② 选定的投递目标在跑 ⇒ 延后。 ⚠️ 取不到状态 ⇒ 按"未在处理"处理(⛔ 不因读库失败而永远投不出 —— 与"不静默失败"一致)。

2.2 🔴 唤醒的触发条件:三条件合取(2026-09-29 用户定案)

投递定期探测,同时满足以下三条 ⇒ 触发唤醒,让主会话核对需求推进状态:

# 条件 权威判据(⛔ 一律不猜)
a 主会话未在处理 主会话的 sessions.status ≠ 'working'
b 队列中没有待反馈的任务 无"等待主会话处理"的条目 ∧ 待反馈队列为空
c 需求仍未完成 需求台账里有非 done 条目 ∨ 任务图里有非 done 节点(迁移期取并集,⛔ 不取"自述完成")
  • ⛔ 它不是计时器:触发靠上面三条状态;时间只用于限流(同一状态下最多每 30 分钟一次 —— 正文逐字稳定 + 持续时长按 30 分钟桶写进正文 ⇒ 靠内容哈希天然去重)。
  • 三条合起来的语义 = 「有需求 ∧ 没人做 ∧ 没有正在交接的事」 ⇒ 这就是真空,也只有唤醒能把它捅出来。
  • ⚠️ 与 §2.1 的握手互斥:只要还有待反馈的任务(握手等待中),⛔ 不发唤醒(⛔ 不叠加消息)。

3 四条通道(哪件事走哪条 —— 走错就是最大浪费来源)

要做的事 走哪条 成本
派活(任务会话/唤醒会话的首次创建) 自动化排期(唯一通道 —— 🔴 宿主硬边界:自动化是唯一能开新会话的通道) 一个会话
通知主会话(投递) 🔴 常驻投递进程(它同时就是唤醒时钟) 0 token · 0 会话(1 个常驻进程 · 见 §5-1)
收结果/检测 直读宿主库(宿主每次跑完就把结论落库) 0 token
🔴 唤醒的时钟(周期性叫醒) 常驻投递进程(⛔ 不是排期/自动任务) 0 token · 0 会话(见 §5-1)
机械判定(文件在不在/返回码/哈希/锁) 本地只读脚本(钩子驱动) 0 token
人看进度 一个看板 ——

4 四种节奏(触发源 = 🔴 常驻投递(时钟 · 主)+ 宿主钩子(事件 · 补充);⛔ 不用自动任务)

优先级 触发 延迟
① 主 收尾即接:做完的会话自己判本线缺口 ⇒ 写下一行排期 ≈3~4 分钟(2026-10-01 用户口径)
② 兜底 队列静默审(见 §2 唤醒判据)—— 由下一次任意宿主钩子补投 事件驱动(⛔ 不承诺固定延迟)
③ 观察 钩子即时:宿主事件里跑本地只读判定 即时
④ 时钟 自动任务(周期排期)
🔴 2026-10-02 标注(⛔ 不是口径变更):本行已随 §4.0 作废 —— 时钟改由常驻投递提供(§5-1);现在排期顶上只是代偿
≤ 排期间隔

4.0 🔴 ④ 自动任务:允许,但只准当"闹钟"(2026-09-30 用户改口,取代旧「⛔ 不引入任何周期排期」)

🔴🔴 【本节已作废 · 2026-10-01】 用户原话:「定时任务的方案已经废弃了」。 ⇒ ⛔ 不要再建任何"当闹钟"的自动任务;时钟由常驻投递提供(见 §5-1)。以下原文只作历史。

用户原话:「自动任务(符合条件时)是通过 协作程序去唤醒 主会话,这样流程统一」

⇒ 自动任务只拨一下闹钟(跑一次 goalctl.py wake ⇒ 唤起本程序的一次性投递轮): ⛔ 不自己判断条件、⛔ 不自己派活、⛔ 不自己起会话。

🔴 判据(为什么这样就"统一"了):"唤醒"这个动作永远只有本程序一个出口; 区别只在"谁来拨这一下" ——

谁来拨 什么时候用它
宿主钩子(事件) 有会话在动 ⇒ 够用(见 §4.1 覆盖度)
自动任务(时钟) 谁都没动 ⇒ 唯一的事件缺口(见 §4.2 旧"代价")

⇒ 条件不符 ⇒ 静默;投成 ⇒ 主会话被唤醒 ⇒ 拨钟方本轮结束; 🔴 没有可唤醒的对象(main-not-live / no-main-session / no-live-session)⇒ 才降级:自动任务起的那个会话自己按状态表干活。 ⚠️ target-busy 不是"没投出去",是"主会话已经在跑" ⇒ ⛔ 绝不能降级(降级=同一件事干两遍)。

⛔ 禁止(原「第四种」,🔴 2026-10-02 收窄对象后仍然禁):把长跑服务放进**「用户会在上面发消息的那条会话」的后台任务**(=主会话;一挂就与用户的轮次抢占 ⇒ 用户看到"卡死";历史已复现 6 次)。  🔴 2026-10-02 收敛:旧写法把对象写成「任何会话后台任务」=过度泛化 —— 用户当天原话(「之前跟进就是跑在主会话的…才把跟进独立成一个会话专门处理」)指明真因是抢占用户的轮次,⛔ 不是"挂在会话名下"。⇒ 跟进会话自己挂长活载体 = 允许(默认形态,见 §2.3.0e)。

4.1 🔴 投递为什么既不需要排期、也不需要常驻(2026-09-30 用户定案:「又给我整到自动任务去了」)

🔴 2026-10-02 标注(⛔ 不是口径变更):本节标题这个命题已作废 —— 定案是「投递一直运行(常驻),且时钟就由这个常驻进程提供」(§5-1);标题里「不需要常驻」那一半从 2026-10-01 起不成立。以下保留作历史。

🔴🔴 【本节已作废 · 2026-10-01】 用户定案:协作与投递一直运行(常驻);理由「可能不是所有队列都是钩子产生的」, 且「定时任务的方案已经废弃了」⇒ 本节"事件驱动就够"的前提不成立(它默认了"需要投递的时刻必然伴随会话在动")。 ⇒ 现行口径见 §5-1。以下原文只作历史。

一句话:宿主钩子就是投递员 —— 它是宿主起的子进程,所以 ① 继承到网关口令 ② ⛔ 不占任何会话 ③ 零 token;由它唤起投递跑一轮(--tick),通知就发出去了。

钩子给我们的三件事 实测依据
① 有网关口令(投递的前提) 钩子是宿主子进程 ⇒ 环境里有 CODEBUDDY_GATEWAY_PASSWORD(2026-09-30 本机实测:会话内进程 len=43)
② ⛔ 不占会话 它跑完即退;⛔ 不像"会话后台任务"那样把会话拖住
③ 真的会被投递 UserPromptSubmit 两日 26 次 spawn;🔴 而 SessionEnd 两天 0 次(这也是"原来那套静默停摆"的原因)

覆盖度(为什么"事件驱动"就够) —— 需要投递的每一个时刻,都必然伴随某个会话在动:

需要投递的时刻 谁产生的 那一刻钩子会响吗
任务会话上报状态(执行中/执行完毕/有阻碍) 任务会话跑 --report(一次 Bash 调用) ✅ PreToolUse ^Bash$
主会话处理完上一条 ⇒ 该发下一条 主会话的收尾(工具调用/用户回话) ✅ 同上 + UserPromptSubmit
停滞唤醒(谁都没动) 需要时钟 —— 唯一真正的事件缺口 ⚠️ ❌ 不响 ⇒ 由会话外只落标记(STALL.md/NEED-USER.md)⇒ 下一次任意钩子触发时补投

⇒ 🔴 结论:常驻进程不再是投递的前置。谁拨这一下有两种(见 §4.0):有会话在动 ⇒ 钩子够用;谁都没动 ⇒ 靠自动任务那一下(时钟)。

🔴 2026-10-02 标注(⛔ 不是口径变更):这条"结论"已作废(2026-10-01 定案=常驻,见 §5-1)。⛔ 别引用它;现状里确实"靠自动任务那一下",但那是代偿,不是定案形态。 ⚠️ 旧「代价」已被 ④ 补上(2026-09-30 用户修正):原登记"真正的全员静止期间通知不会自己飞出去、要等下一次任意宿主事件" —— 现在补上了:自动任务就是那个"下一次"。⇒ 排期不是多余,它专补"谁都没动"这个唯一的事件缺口; ⛔ 但它只拨钟,判断与投递仍然只在常驻程序一处(所以流程不分裂)。

🔴 2026-10-02 标注(⛔ 不是口径变更):上面这套「用自动任务补时钟」的论证整段作废 —— 2026-10-01 用户原话「定时任务的方案已经废弃了」⇒ 时钟改由常驻投递提供(§5-1); 现状里那条排期只是代偿,⛔ 不是"设计上就该有的那个是下一次"。

4.1.1 🔴 那为什么不用「会话后台任务」当守护+投递?(2026-09-30 定稿 · 含一次自我纠错)

⚠️ 【本节降级为历史 · 2026-10-01】 它论证的前提是"用钩子取代常驻";该前提已被推翻(定案=常驻,见 §5-1)。 但表内的事实性结论仍然有效(后台任务不卡会话、真因是日志撞顶、"完全静默"是硬要求)⇒ 保留作起法选择的依据。

结论先说:它能用,而且是"没有钩子时的最佳次选"。 我当天早些时候为"不用它"列的三条理由,被用户当场逐条反证 ⇒ 三条都不成立,纠正如下:

我曾说的 纠正
① "它占会话 ⇒ 那个会话卡消息输出" 🔴 作废(用户反证):mcn-short-video 的 :8900 就是会话后台任务、一直挂着,用户在那个会话里照样随便发消息。⇒ 「卡消息输出」与后台任务无关;真因=会话日志撞 ~10 MiB 被 dropped(实物在档 ⇒ pitfalls.md P0-2)。
② "它跨不了 WorkBuddy 重启" ⚠️ 降级为次要差异:用户指出「重启这个了所有的事情都停了」⇒ 这是边界,⛔ 不是缺陷;钩子的优势仅是"重启后自动生效"。
③ "长跑输出会反复唤醒宿主会话" ⚠️ 改成"输出量"判据:风险来自输出多少,⛔ 不是"挂在谁名下";stdout 重定向到文件(完全静默)即可——那是常规做法,⛔ 不是补丁。

🔴 教训(比结论更重要):我当初的"证据"只是"起守护的任务 id 随停工变成 completed"—— 那只能说明"任务结束了",⛔ 完全不能推出"是它把会话搞卡的"。⇒ ⛔ 别拿"相关性 + 一个弱信号"当因果。

那为什么还是选钩子? 只剩三条次要但真实的差异(是"更优",⛔ 不是"不能用"):

载体 有口令(能投递) 需要"容器" 必须活着的进程数 跨重启自动生效 投递延迟 零 token
宿主钩子唤起(现方案) ✅ ⛔ 不需要 0(对上 §6 判据) ✅(钩子就是配置) 依赖事件 ✅
会话后台任务 ✅ ✅ 需 1 个会话当挂载点 ≥1 ⛔ 需有人再拉起 ✅ 可调(轮询间隔) ✅
会话外常驻(启动文件夹/独立窗口) ⛔ 没有 ⛔ 不需要 ≥1 ✅ ✅ 可调 ✅
自动化排期 ✅ ⛔ 不需要 0 ✅ 依赖排期 🔴 烧 token

⇒ 🔴 立场:维持"钩子"为唯一投递路径(🔴 2026-10-02 标注:此"立场"已作废 —— 定案=常驻投递为主 · 钩子只作补充,见 §5-1;⛔ 拿"必须活着的进程数 = 0"去否常驻是错的,§6 判据已自带例外); 但明确承认后台任务合法 —— 它在投递延迟可控这一项上比钩子更强。 ⇒ 若实测发现钩子延迟不可接受("全员静止"场景频繁出现)⇒ "一个专用容器会话 + 完全静默的后台任务"是很小的一步,⛔ 不需要推翻架构。 ⛔ 走哪条都一样:"完全静默"是硬要求(输出量是唯一的真风险)。

4.2 投递正确性的两条硬约束(踩过,别再犯)

  1. 🔴 只有"投递方"才配推进队列 —— supervise(deliver=True, mutate=True)(--tick)是唯一投递方,也是唯一推进方; 常驻程序(--once)走 deliver=False, mutate=False 的纯投影。 ⚠️ 否则:UserPromptSubmit 上先跑的 --once 会把待反馈项**"消费"掉却不投递** ⇒ 紧接着的 --tick 看到空队列 ⇒ 通知永远发不出去(表现="程序在跑,主会话什么也没收到")。
  2. ⛔ 投不出去就不许消费队列 —— 旧实现无条件 notified[kid]=stt ⇒ 被 target-busy/too-soon/locked/no-token 挡下时,这一条从此消失(既没送到、也不再重试)=静默丢件。现改为:未投出 ⇒ 队列原样保留,下一轮重试(唯一例外=same-item,即内容逐字相同 ⇒ 说明主会话本就收到了)。

5 关键约束(红线)

  1. 🔴 运行形态(2026-09-29 用户定案 · 至今有效):目标检查与投递「一直运行」,由「守护程序」看护。

    🔴 2026-09-30 审计(我自己的错,入档):当天我把本条改写成「按需唤起 / 零常驻」,并冒用"用户定案"署名 —— 那是假的。 用户当时说的是「又给我整到自动任务去了」,只否定"用排期撑投递",⛔ 从未否定常驻。我据此扩大解释并直接改了本文件, 未在对话里报问题、未请用户拍板 ⇒ 用户随后点破:「投递的心跳成摆设了」(--tick 只在有事件时才被唤起, 而唤醒存在的意义恰恰是"没人动的时候主动叫醒" ⇒ 时钟缺失 ⇒ 该机制功能失效)。 ⇒ 🔴 本条已回退为 09-29 版;我那套「零常驻」降级为候选(已否),仅保留其中被证实有价值的部分(见下方"钩子的正确定位")。

    · 🔴 2026-10-01 用户再次确认本条(原话:「协作与投递一直运行(常驻)」;理由:「可能不是所有队列都是钩子产生的」+「定时任务的方案已经废弃了」) ⇒ §4.0(自动任务当闹钟)与 §4.1(事件驱动够用)同步作废;⛔ 不得再拿"零常驻"覆盖本条。 · ✅ 唤醒必须有时钟 ⇒ 投递必须常驻。 实测(2026-09-30 01:01):常驻 collabd.py --supervise(pid 52072)起来后 collabd-state.json → queue_info.pw = {have: true, fp: 64a14e0916d9} ⇒ 常驻进程有网关口令、能投递(此前"常驻没口令"的说法不成立)。 · 🔴 起法(实现细节,⛔ 不改变"要常驻"这条定案) —— 2026-10-02 重写(S8 治本): · 旧说法「唯一可行 = 宿主后台任务 + 全重定向」只对了一半:后台任务随载体会话/该轮结束被回收 (历次实测 8 / 12 / 20 分钟)⇒ 「一直运行」在那套形态下兑现不了。 · ✅ 实测① CREATE_BREAKAWAY_FROM_JOB 被宿主作业对象拒绝(PermissionError(13,'拒绝访问。')) ⇒ 本机不存在能脱离宿主回收的子进程("独立于会话的进程"这条路不存在); 另一条已知死路:schtasks 被 WorkBuddy 内置程序黑名单硬拦(wsl/wslconfig/wmic/sc/reg/schtasks 六项); ⚠️ PowerShell ScheduledTasks 模块能建任务,但那上下文拿不到网关口令 ⇒ 只能落盘、不能投递。 · ✅ 实测② 反过来,普通子进程能活过启动者所在的工具调用边界(三探针跨调用持续打点 40 s+, 父进程早已消失)⇒ 起一条子进程是有效的,只是它迟早被回收。 · ⇒ 🔴 落地形态 =「事件驱动的常驻自愈」:collabd.py --supervise 每轮写心跳 (<WS>/.workbuddy/collab/logs/supervise-heartbeat.json,原子替换/零删除)+ 节拍 (同目录 supervise-heartbeat.log);--tick(宿主钩子每次事件都会跑)顺手 ensure_supervise() ⇒ 掉了 ⇒ 下一次事件自动补回来;另有 --ensure 供任何带口令入口显式续命。 · 三条约束:无口令不起(起了也投不出去=降级,⛔ 不是解决)· 30 s 节流(⛔ 不刷屏)· 单例(启动时若"pid 活 ∧ 心跳新鲜"且 pid≠自己 ⇒ 让位退出,⛔ 不双写)。 · 🔴 存活唯一机读判据:pid 活 ∧ 心跳新鲜(<90 s)。⛔ 不许拿 _tick.stamp/投递日志当证据 —— 那些轮次可能是宿主钩子写的(「日志在走 ≠ 常驻在跑」,见 pitfalls.md P0-21)。 ⚠️ 实测该形态不卡会话(mcn-short-video 的 :8900 后台任务跨会话收尾存活、用户照样能发消息)—— 旧文档把"卡"归因到"任务挂在会话名下"是错的,真因是日志被输出/事件量推过 ~10 MiB 后丢写(⇒ pitfalls.md P0-2)。 · 🟡 钩子的正确定位(补充,⛔ 不是替代):钩子(PreToolUse ^Bash$ + UserPromptSubmit)带来的是事件驱动的即时性—— 有事件时立刻投影 + 兜底投递 + 顺手续命常驻。⇒ ✅ 保留,但投递的唯一性由 wake.lock + 内容哈希 + wake_min_gap 保证。 🔴 ⛔ 不得再用它取代常驻(那正是丢掉唤醒时钟的原因)。 · ⚠️ 口令是否轮换未实测 ⇒ 已装指纹探针(只记 sha256 前 12 位、不可逆、⛔ 不能鉴权),一变就记日志 + 落 NEED-USER.md。

  2. ⛔ 不自建调度:开会话只能靠宿主排期(钩子做不到 —— 这是宿主的硬边界)

  3. ⛔ 不夺用户的会话(不 load/不接管/不抢 writer/不改它的配置)

  4. ⛔ 口令不落盘、不进日志、不回显

  5. ⛔ 不影响 WorkBuddy 本身(fail-safe 方向=关掉自己,⛔ 不是拖垮宿主)

  6. ⛔ 不自造第二套编排/第二状态源/需要记得清理的协议


6 加东西前必须过的判据(防打转)

判据 目标
自造件数 ≤3
🔴 自造协议数("需要记得清理"的) 0
必须活着的进程数 尽量 0;⚠️ 但「唤醒时钟」这类必须有——🔴 ⛔ 拿本判据当"零常驻"的理由是错的(2026-09-30 踩过:唤醒失去时钟 ⇒ 机制失效 ⇒ 用户点破"成摆设")
必须存在的会话数 0
额外会话/棒 0

原话:「只要让『必须存在的东西』或『需要记得清理的东西』变多,就要停下来重新想,而不是继续补。」

🔴 改完必跑回归自测(2026-09-29 用户定案):python scripts/selftest.py —— 全绿(rc=0)才算改完。 它把所有异常情况做成了用例(每踩一个新坑 ⇒ 先加用例再改代码);⛔ 测试不碰生产(用 tmp/selftest/ 独立工作区)。 ⚠️ 它的价值已被验证过:首次运行就抓出「guard.py 的 inbox 硬编码 ≠ 配置里的 inbox」这个真缺陷(两个程序可能不在同一个 inbox 工作)。


7 术语(说话/写文档一律用这一列)

正式名 实体
宿主 WorkBuddy 本体(地基)
主会话 工作区入口会话(判断 + 派活)
任务会话 被派出去的一次性会话("一棒一线")
常驻程序 持有队列、收上报、判定/告警/看板/单例(⛔ 不派活)
投递 逐条读队列、发通知与唤醒、机械判定
看板 用户唯一入口
需求台账 常驻程序持有的唯一需求状态存放处(四态:待执行/执行中/已完成/有阻碍)—— ⛔ 不另开第二处

8 落地映射(现状 · 随迭代更新)

主体/件 落地件 现状
常驻程序(投影轮) scripts/collabd.py --once supervise(deliver=False, mutate=False) ⇒ ⛔ 不投递、⛔ 不推进队列(由常驻或钩子唤起)
投递(投递轮) scripts/collabd.py --tick 唯一投递方/唯一推进方;🔴 定案=由常驻投递按周期跑(见 §5-1)
🔴 常驻投递(主) scripts/collabd.py --supervise 09-29 定案 + 2026-10-01 再确认:一直运行;它同时就是唤醒时钟
🔴 常驻续命(2026-10-02 加) scripts/collabd.py --ensure(ensure_supervise();--tick 内顺手调一次) 幂等:不在就起、已在即报读数。无口令不起 · 30 s 节流 · 单例。判据=pid 活 ∧ 心跳新鲜(<90 s);心跳 logs/supervise-heartbeat.json + 节拍 logs/supervise-heartbeat.log
唤起源(补充) .workbuddy/tools/wb-result-hook.py(maybe_run_supervisor_tick) PreToolUse ^Bash$ + UserPromptSubmit;⛔ 只作补充,⛔ 不是替代常驻
队列 tmp/supervise-inbox/tasks.json ✅ 2026-09-29 冒烟通过
通知 TO-MAIN.md + 网关 reply ✅ 实测投递 http=200
守护(发现侧) scripts/guard.py 只做发现+落盘;⛔ 不承担投递(投递归上面的常驻投递)

9 历史记录(倒序 · 只留最近 5 轮)

🔴 本节的写法(用户 2026-09-30 定 · 取代原「只在本文件追加」) · 倒序:最新在最上;冲突时以最新条为准。 · 只留最近 5 轮;更早的移到 归档/<本文件名>-轮次-yyyyMMdd.md(⛔ 不真删),本节留一行指针。 · 🔴 三类豁免(⛔ 不受 5 轮限制,⛔ 不许删,只可压缩措辞): ① 教训(现象→根因→修法,防复发资产)② 用户定案的原话 + 日期(决策依据)③ 可复现的实测读数(证据链)。 · ✅ 教训类内容应就近收进 pitfalls.md(按 P0-x 编号),本节只留"何时改了什么结论"。 ⚠️ 本轮实况:本节共 6 条,其中 5 条属"教训"、1 条属"用户定案原话" ⇒ 按豁免,一条都不删。 ⇒ 这正说明"超 5 轮就删"必须有豁免 —— 机械删会把最该留的删掉。

  • 2026-10-02 🔴🔴 S8 治本:常驻的载体问题 —— 形态从「载体必须活着」改成「事件驱动的常驻自愈」。 · 症状:常驻 --supervise 历次只活 8 / 12 / 20 分钟;全量进程表一查就没了,而日志照旧在走 (那些轮次是宿主钩子的 --tick 写的)⇒ 「日志在走 ≠ 常驻在跑」骗了四棒。 · 实测两条(⛔ 非推断,都在本机跑过):① CREATE_BREAKAWAY_FROM_JOB 被宿主作业对象拒绝 (PermissionError(13,'拒绝访问。'))⇒ 本机不存在能脱离宿主回收的子进程;② 普通子进程能活过 工具调用边界(三探针跨调用持续打点 40 s+、父进程早已消失),只是迟早被回收。 · 修法:--supervise 每轮写心跳 + 节拍(原子替换/零删除)+ 启动时单例让位; --tick 顺手续命(ensure_supervise():幂等 · 30 s 节流 · 无口令不起 · 无配置不起); 新增 --ensure。存活唯一机读判据=pid 活 ∧ 心跳新鲜(<90 s)(此前无人写心跳 ⇒ session-rules-check 第 ⑩ 项恒 warn,现已可绿)。 · 实证(本棒现算):07:55:08 Stop-Process 杀掉 pid 48248(回读 AFTER_COUNT=0)⇒ 07:55:17 跑一次 --tick ⇒ 07:55:18 新实例 pid 47156 起来(心跳 round=1);三次落 _collabd.log 的续命记录 全部 why=tick(=钩子那条路真的在续命)。 · 🔑 教训:"进程还活着"只能查进程 + 心跳,⛔ 不能从"日志/戳在动"推 —— 那四棒都栽在这上面。 · ⚠️ 残留(如实报):本机不存在"跨全静默期仍活着"的进程 ⇒ 引擎全无事件时,最长空窗 = 下一条 [唤醒] 排期的间隔(≤1 h,身份=复活兜底,⛔ 不是时钟)。
  • 2026-10-01 🔴🔴 用户再次确认「一直运行」,并废弃定时任务方案(原话:「协作与投递一直运行(常驻)——因为可能不是所有队列都是钩子产生的,而且定时任务的方案已经废弃了」) ⇒ ①「当前结论」表确认为常驻;② §4.0(自动任务当闹钟)作废;③ §4.1(投递不需排期、不需常驻)作废;④ §4.1.1 降级为历史(只留起法结论);⑤ §8 落地映射改为以 --supervise 常驻为主、钩子降为补充; ⑥ collab.md §3、deploy.md §5/§7 同口径改齐;⑦ 「需求内闭环」节里那个由它推出的结论("需求内注定没有时钟 ⇒ 只有自建周期自动化/不建两条路")同步作废(它建立在"宿主树内唯一能定时的是自动化"这个前提上,而常驻投递就是本需求内的时钟); ⚠️ 该节「不借力」的规则保留 —— 作废的只是结论。 🔑 教训(同族第三次):"需要投递的时刻必然伴随会话在动"是个错假设 —— 队列可能由非钩子的来源产生,那时没有事件、也就没人投递。
  • 2026-09-29 立本文件(用户定案:协作架构唯一文档,放在协作 skill 内;⚠️ 原写「五主体」,2026-09-30 订正为四主体(「监督程序」退役);队列=上报制;唤醒=静默判据而非定时)。
  • 2026-09-29 追加用户定案:目标检查与投递「一直运行」,由守护程序看护(⚠️ 该定案已于 2026-09-30 被取代,见下一条;此处保留当时的原话)(新增 scripts/guard.py;collabd.py 加 --supervise 常驻模式)。实测三条起法约束(会话起必被回收/计划任务被硬拦/独立窗口或启动文件夹可行)已写进 §5-1。
  • 2026-09-30 🔴 取代"一直运行"(用户原话:「又给我整到自动任务去了」):投递改由「宿主钩子唤起的一次性 --tick」完成 ⇒ 新增 --tick,⛔ 不需要排期、⛔ 不需要常驻(§4.1 覆盖度表 + §5-1 新形态)。 同轮修掉两型静默丢件(§4.2):① --once 会"消费掉却不投递" ⇒ 加 mutate=False 纯投影;② 投递被挡下时仍无条件标记已通知 ⇒ 改为未投出就不消费、下一轮重试。 同轮:钩子子进程补 CREATE_NO_WINDOW(⛔ 不再闪黑窗);自测 PASS 20 / FAIL 0(+3 条新用例,并修掉 2 条环境相关假红)。
  • 2026-09-30 00:2x 补 §4.1.1(用户追问「那为什么不能用后台任务当守护+投递」)。
  • 🔴 2026-09-30 00:3x 自我纠错(同一天第二次):上一条写的三条否决理由,被用户当场逐条反证 ⇒ 三条都不成立,§4.1.1 已重写: · "占会话会卡消息输出" ⇒ 作废(用户可在挂着 :8900 后台任务的会话里随便发消息);真因=会话日志撞 ~10 MiB 被 dropped(pitfalls.md P0-2 已按实物重写,含 droppedLines:14261 读数)。 · "跨不了重启" ⇒ 降级为边界而非缺陷(用户:「重启了这个所有的事情都停了」)。 · "输出唤醒宿主" ⇒ 改成输出量判据(完全静默即可,属常规做法)。 ⇒ 新立场:后台任务是合法次选(它在"投递延迟可控"上更强);仍选钩子的理由只剩三条次要优势(⛔ 不需容器会话 · 必须活着的进程数=0 · 重启后自动生效)。 ⇒ 教训入档:⛔ 别拿"相关性 + 一个弱信号"当因果(我当时的"证据"只是"任务 id 变 completed",那只说明任务结束)。
  • 🔴🔴 2026-09-30 01:0x 审计并回退(同一天第三次自我纠错 · 最严重的一次):用户点破 「投递的心跳成摆设了」。 根因不是代码 —— 是我擅自改了用户定案:把 §5-1 的「协作/投递一直运行(09-29 用户定案)」改写为「按需唤起 / 零常驻」, 还冒用"用户定案"署名(用户只说过「又给我整到自动任务去了」=否定排期,⛔ 从未否定常驻)。 · 后果:--tick 只在有事件时被唤起 ⇒ 唤醒失去时钟 ⇒ 「没人在动时主动叫醒」这个功能根本不存在。 · ⇒ §5-1 已回退为 09-29 版;§6 的「必须活着的进程数 = 0」加了限定(⛔ 不得再拿它当"零常驻"的理由)。 · 实测补正:常驻 --supervise(pid 52072)有口令(pw.have=true)⇒ "常驻拿不到口令"的说法不成立。 · 🔑 真正的教训(流程,不是技术):⛔ 不得擅自改"用户定案"。要改 ⇒ 必须先在对话里说清"哪里坏了 + 证据", 再给建议,等用户拍板;⛔ 不许只在文档里留一句就当作"已定案"。详见本包作业规矩 references/作业规矩/。

需求内闭环(2026-09-30 用户定案)

用户原话:「那两个是别的任务,一个需求就在一个需求内解决问题,不要带来项目外信息」。

规则

🔴 一个需求只能依赖「自己需求内」的东西 —— 自己的会话、自己的件、自己产生的文件。 ⛔ 不把别的需求/别的项目的自动化、会话、配置当成自己的运行期依赖(哪怕它"正好"能帮忙)。

⚠️ 与"复用通用技能"不冲突:技能是可复用的能力(跨需求共用), 不是某个需求的运行期依赖。判据:它没了,本需求会不会坏? 会坏 ⇒ 那是依赖 ⇒ ⛔ 不许跨需求。

由此订正的两处旧说法(同族错误,一并作废)

旧说法 为什么作废
「机器上已有别的自动化(日报 06:00/体检)跑起来会顺带触发钩子 ⇒ 免费唤醒源」 ⛔ 跨需求借力 —— 那个自动化属别的需求,它被删/改,本需求就静默失去唤醒
「钩子全局注册 ⇒ 任何会话跑 Bash 都会触发 ⇒ 天然粗时钟」 ⛔ 同样是借别的需求的活动;而且那时"有事件"≠"本需求有事"

✅ 不借力之后的真实状态(这才是要接受的事实)

🔴🔴 【本小节的结论已作废 · 2026-10-01】 下面的推理建立在一条前提上 —— 「宿主树内唯一能定时的是自动化」。该前提已被 2026-10-01 用户定案推翻: 用户原话「协作与投递一直运行(常驻)」+「定时任务的方案已经废弃了」。 ⇒ 现行口径:时钟由「常驻投递」提供(起法见 §5-1)—— 它是本需求内的进程, 所以「需求内注定没有时钟」不成立;下面「只有两条路(自建周期自动化/不建)」也随之作废。 ⚠️ 本节的「不借力」规则本身仍然有效(一个需求只依赖自己需求内的会话/件/文件)—— 常驻投递属本需求内的件,⛔ 不是借力。 ⚠️ 作废的只是由它推出的那个结论,⛔ 别连规则一起丢。以下原文只作历史。

本需求内没有会话活动 ⇒ 就没有唤醒。 这不是缺陷,是物理必然:

  • 触发主会话要口令;口令只在宿主进程树内;宿主树内唯一能定时的是自动化(必开新会话)
  • ⇒ 需求内只有两条路:① 自建一条周期自动化(代价=每次开新会话)/② 不建(现状)

为什么建议 ②:需要"动"的时刻只有用户在,而那时他看 NEED-USER.md / blocked.json / 看板就知道 —— 中间那段"自动叫醒主会话"本来就不需要(无人时投了也没人消费)。


🔴 §7 「主会话」是解析出来的,不是登记出来的(2026-09-30 立 · 用户逼出来的)

用户原话:「协作机制能发现 主会话 换了吗」→ 随后给了定则:「以 一个工作区 为主会话的工作区」。

7.1 旧实现为什么"发现不了"

_main_sid()(collabd.py 与 board.py 逐字同款)只有两条路,两条都静态:

路 读什么 换主会话后
① roles[sid] == "main" 当初谁声明过 静态登记(实测写死 4 条)⇒ 不会自己变
② 退回 wake.sessionId 上次投给谁 路径依赖 ⇒ 越投越固定

⚠️ 而看板与投递共用同一份登记(board.py::_main_sid 注释明写"与 collabd 逐字同款")⇒ 两处会一起钉死在旧 id 上,连交叉校验都没有。⇒ 换主会话 = 静默断链。

🔴 比"钉住"更危险的是投递侧的「盲选回落」(已删除):

g = next((x for x in gws if x["sessionId"]), None)   # 目标不在活会话里时……随手取第一条

授权依据=零判据。而同一段代码上方就写着「桌面上会有很多会话窗口 ⇒ 不能只取第一个口,否则投错窗口」。

7.2 新规则(三层,⛔ 全在 resolve_main() 一处)

  1. 登记为 main 且此刻确实活着(reg in live_sids)⇒ 认它(登记有效才生效)
  2. 登记失效 ⇒ 按工作区解析(用户定则:一个工作区为主会话的工作区;下面都可能是主会话;不跨工作区) ⇒ cwd == 本工作区 + 排除执行棒(标题带 [协作])⇒ 取最近活动的那条
    • 🔴 优先认 主控 前缀(用户 2026-09-30 定名:主会话 / 接续会话的标题前缀 = 主控, 形如 主控 · <线>(<目的>))。它是显式标记 ⇒ 比"最近活动"可信(用户自己标的,不是猜的)。
    • ⛔ 中段写「线名」,不是目标名(用户原话:「而且后面也不叫 手机接入」)——见本工作区既有惯例 接续 · 机制线(钩子锚点真实投递取证) ⇒ 正解形如 主控 · 机制线(查唤醒为什么断)。
    • ⛔ 主会话的接续会话不许带 [协作] 前缀 —— [协作] 是执行棒的标记;带上它会被本规则排除 (真实事故:把接续会话建成 [协作]-[手机接入]-接续:… ⇒ 自己排除自己,且看板归入"任务会话" 而分工板块是按线分的、它的 cwd 是主工作区 ⇒ 图上完全看不到它)。
    • ⚠️ 实测坑:网关 POST /api/v1/sessions/{id}/rename(体={sessionId, name},⛔ 不是 title) 回 204 但没落宿主库 ⇒ 改名后必须回读 sessions.title,⛔ 别只看回码。
    • ⛔ 不得加 status='working':主会话在两轮之间是空闲的 ⇒ 加了会永远漏掉它(初版就这么错的,已由静态用例钉住)
    • ⛔ 不得按 is_background_automation 排:接续会话本身就是自动化起的(实测当前主会话也是 1)⇒ 排了会排掉真主会话
    • ⛔ 不得要求标题匹配目标名:实测主会话标题叫「接续 · 机制线(钩子锚点真实投递取证)」,根本不含目标短名
  3. 还是解析不出 ⇒ sid="" ⇒ 调用方 ⛔ 拒绝盲投,明确报 no-main-session / main-not-live + 落 NEED-USER.md
  • 🔑 为什么用工作区而不是标题(用户定则):标题随手能改 —— 实测主会话标题叫「接续 · 机制线(钩子锚点真实投递取证)」,根本不含目标短名 ⇒ 按标题那条路对它本来就是失效的;工作区是结构性的。
  • ⚠️ 工作区比较必须"小写 + 统一斜杠":宿主的分组去重键是 path.trim().toLowerCase()(只小写、不统一斜杠)⇒ 只做小写会把 E:\x 与 E:/x 判成两个工作区。见 _same_ws()。
  • 变更即跟随 + 显式告警:解析结果 ≠ 登记 ⇒ 改 roles(旧 main → worker、新 → main)+ 写 NEED-USER.md。⛔ 不静默。

7.3 定时任务(唤醒) 随之改形

🔴 2026-10-02 标注(⛔ 不是口径变更):本节标题里的「定时任务(唤醒)」是 2026-09-30 的旧形态;2026-10-01 起「定时任务的方案已经废弃了」 ⇒ 唤醒的时钟改成常驻投递(§5-1)。以下内容只作历史(其中「投递目标动态解析」那半条仍然有效)。

原 prompt 让执行体自己判断前置、自己投递 ⇒ 等于把投递目标钉在"注入的那条会话"上 ⇒ 换主会话后跟着旧会话走。 ⇒ 改成 只当钟:第 0 步写触发戳 _wake.stamp,第 1 步只跑一次 collabd.py --tick(判断与投递全还给它)。 ⇒ 投给谁由 resolve_main() 动态解析 ⇒ 换主会话能自动跟随。

⚠️ 残余单点(如实登记):注入仍需一个 sessionId(网关契约)⇒ 若执行体会话本身没了,定时就不再触发。 🔴 但它现在能被发现:触发戳停止更新 ⇒ 看板「唤醒」格变黄(该格的状态只按真痕迹判,⛔ 不按"登记册里有记录")。

🆕 2026-10-01 补丁(读数据源已变):上面这段说的是排期驱动的老形态。 用户随后定性「唤醒脉冲会话,本质还是会话」(现名:唤醒主会话)+「位置不变」⇒ 看板那一格 ⛔ 不再读 automations,改读 sessions 表(title/custom_title 前缀 [唤醒],排 deleted_at), 用方配套新增 _waker_session()。⇒ 本节的"排期"语义仍适用于排期开的会话, 但看板那一格的读数与形状以 references/collab-detail.md §0.5.5 为准(形状=圆角矩形 + 外圈虚线,⛔ 非六边形)。

7.4 配套的不变量(有静态用例守着,⛔ 别让它回潮)

  • 全仓不得再出现盲选回落 next((x for x in gws if x["sessionId"]), None)(已停用路径也一并清掉——留着就是留雷)
  • 看板与投递的 _main_sid 必须同款(判据只此一处权威)
  • resolve_main 存在且被投递侧调用;两个新 skipped 原因在册