# 统一唤醒出口(2026-09-30) > 用户口径(本轮): > **「自动任务(符合条件时)是通过 协作程序去唤醒 主会话,这样流程统一」** + > **「技能里已写明『投递不靠自动任务』:这个过时了,现在允许自动任务」** > 落地物:`goalctl.py wake`(新)+ 两条排期改名/改提示词 + **技能侧过时规则纠偏**。全文读数均为实测。 --- ## 0. 结论(三行) 1. 🔴 **判据一句话**:**「唤醒(投递)」这个动作只有一个出口 = 协作程序**;区别只在**"谁拨这一下"** —— **钩子**(有会话在动时)/**自动任务**(谁都没动时的时钟缺口)。⇒ 这就是"流程统一"。 2. 自动任务**只准当闹钟**:`goalctl wake` ⇒ `collabd --tick`。⛔ **不自己判条件、⛔ 不自己派活、⛔ 不自己起新会话** —— **除非**闹钟报"**没有可唤醒的对象**",才降级。 3. 技能侧那条旧规则(「投递 ⛔ 不靠自动任务排期」)**已按用户口径改为「允许,但当闹钟」**(4 处)。 --- ## 1. 统一后的流程 ``` ┌─ ① 宿主钩子(事件)──┐ ← 有会话在动时,够用 需求在途 ──需要唤醒─┤ ├─→ 协作程序(唯一唤醒出口)─→ reply 唤醒【主会话】 └─ ② 自动任务(时钟)────┘ ← 谁都没动时(唯一事件缺口) (只拨钟:goalctl wake) ``` | 谁拨 | 何时用 | 拨完干什么 | |---|---|---| | **宿主钩子** | 有会话在动(上报/收尾/用户回话) | 跑 `--tick` ⇒ 判断+投递 | | **自动任务** | **谁都没动**(钩子的唯一缺口) | 跑 `goalctl wake` ⇒ 同一件事,**同一出口** | ⇒ 条件判断**只在协作程序一处**(不散落在排期提示词里)—— 这正是用户要的"统一"。 --- ## 2. 判定三分(=用户说的「符合条件时」) 闹钟(`goalctl wake`)输出 → 拨钟方该干什么: | 闹钟输出 | 含义 | 拨钟方动作 | |---|---|---| | ✅ **`http 2xx`** | **已投递** ⇒ 主会话被唤醒 | **结束**(⛔ 不抢锁、⛔ 不派活、⛔ 不起会话) | | ✓ **`-`**(无待投)/`same-item`/`too-soon` | **条件不符**(没变化/刚投过) | **结束**(零打扰 —— 这就是"符合条件时才唤"的否定面) | | ⏸ **`target-busy`** | 主会话**已经在跑** | **结束** —— 🔴 **绝不能降级**(降级=同一件事干两遍) | | ⛔ **`disabled`** | 本需求已停(`wake_enable=false`) | **结束** | | 🔴 **`main-not-live`**/`no-main-session`/`no-live-session` | **没有可唤醒的对象** | 🔴 **降级**:拨钟方**自己就是那个会话** ⇒ 抢域锁 + 按状态表干活 | | 🔴 **`no-token`**/`no-gateway` | 异常(没跑在宿主进程树里?) | 报告一句,**结束**(需人看) | ⚠️ **`target-busy` 与"没有可唤醒的对象"必须分开** —— 前者是"**已经在跑**"(别插手),后者是"**没人可跑**"(要我顶上)。这两者混了,就会出现"同一件事被干两遍"或"没人干"。 --- ## 3. 落地清单 | # | 动作 | 结果 | |---|---|---| | 1 | 新增 `goalctl.py wake`(**唯一唤醒入口**) | 调 `collabd.py --tick`,把 `deliver=` 字段翻成人话 + **给判定三分**(rc:0 结束/10 降级/2 异常) | | 2 | 两条排期**改名**为 `[协作]-唤醒轮A/B · ai1net-dsh-server` | 🔴 **必须带 `[协作]`** —— 见 §4.1 | | 3 | 两条提示词改为**「拨钟三分」** | 默认动作=拨钟即结束;只有"降级"那一条路才抢锁干活 | | 4 | **纠偏技能侧过时规则**(4 处) | 见 §5 | | 5 | 自测 | **PASS 28 / FAIL 0** | --- ## 4. 两条会**静默坏**的易错点(🔴 本轮实测) ### 4.1 唤醒轮不能让自己被认成"主会话" `resolve_main()` 的兜底判据是:**本工作区里 `last_activity_at` 最新、且标题不含 `[协作]`** 的那条。 🔴 **刚起的唤醒轮活动时间必然最新** ⇒ 必然被选中 ⇒ 协作程序把通知**投给唤醒轮自己** ⇒ **主会话永远收不到**。 - **为什么原名会中招**:排期名 `[本机协作]-唤醒轮A` 里**并不含 `[协作]` 这个子串**(它是 `[本机协作]`)⇒ 排除判据**不生效**。 - **修法(已做)**:排期名带 `[协作]` ⇒ 会话标题继承排期名 ⇒ 被排除。 ✅ ### 4.2 主会话标题必须以 `主控` 开头(不能加方括号) 代码判的是 `title.strip().startswith("主控")`;实测有条会话标题是 **`[主控]-[机制线]-…`** ⇒ **判 False** ⇒ 只能靠"最近活动"兜底 ⇒ **本工作区任何一条会话活动更新就可能把它顶掉**。 ⇒ 命名一律用 `主控 · …`(⛔ 别写成 `[主控] · …`)。这条 SKILL.md §354 已登记,此处再次坐实。 --- ## 5. 技能侧过时规则纠偏(用户:「这个过时了,现在允许自动任务」) | 文件 | 原话 | 改成 | |---|---|---| | `references/architecture.md §4` | 「三种节奏(触发源 = 宿主钩子;**⛔ 不引入任何周期排期**、⛔ 不靠常驻)」 | **四种节奏** + 新增 **§4.0「④ 自动任务:允许,但只准当闹钟」**(含判定三分、`target-busy` 禁降级) | | 同上 §4.1 结论 | 「…**排期更是多余**」+「代价:真正的全员静止期间通知不会自己飞出去」 | 「排期**不是多余**,它专补"**谁都没动**"这个**唯一的事件缺口**」;旧"代价"**已被 ④ 补上** | | `scripts/collabd.py` 模块用法注释 | 「投递=宿主钩子唤起,**⛔ 不靠排期**」 | 加「**同日用户改口:允许自动任务**」+ `deliver=` 判读三分 | | `scripts/collabd.py` `--tick` 注释块 | 「**⛔ 不靠自动任务排期**」 | 追加「2026-09-30 用户改口(取代本句)…」 | | `scripts/selftest.py` 注释 | 「投递⛔ 不靠自动化排期」 | 标注已改口,并写明**下面两条约束与"谁拨这一下"无关、仍然有效** | 🔴 **注意**:改的是**触发源**,不是**约束**。这三条**一条都没放松**: ① `--once` 不许消费队列 ② `--tick` 投不出去就不许消费队列 ③ 投递方唯一。 --- ## 6. 验证记录(实测) ``` ═══ 拨闹钟 · 协作程序判条件并投递(20:27:44)═══ 协作程序结论:tick: item=- awaiting=- phase=- deliver=- consume=- park=- ✓ 判定:**没有需要投的东西**(队列无新变化 / 无停滞)⇒ 静默。 ⇒ 本轮**就此结束**(这就是「符合条件时才唤」的否定面 —— 零打扰)。 wake rc=0 ``` - 已实测两条分支:**静默(`deliver=-`)** 与 **已停(`wake_enable=false` 直接返回)**。 - "投成"与"降级"两条走的是协作程序**既有且已实战过**的代码路径(`_deliver_str` 的 `delivered` / `skipped`),本轮**未人为制造**(⛔ 不扰动生产队列)。 - 技能自测:**PASS 28 / FAIL 0**;顺带清掉 `py_compile` 生成的 `__pycache__`(技能侧自测的**硬项泄漏**,已消除)。 --- ## 7. 能力边界(诚实说) - 🔴 **能被唤醒的前提是"主会话此刻 live"**。实测 worker 寿命 **30 s–6 min**(干完活就回收)⇒ **大概率不 live** ⇒ 闹钟会报"没有可唤醒的对象" ⇒ **降级**。 - ⇒ 要真做到"**唤醒统一走协作程序**",得让**主会话保持活着**(把那个窗口开着),否则多数时候仍落在降级路径上。 - ⚠️ 降级**不是**绕过统一:省掉的只是一次投递,**判断规则仍来自同一张状态表**。 - ⚠️ 本机"需求↔工作区"仍是 **1:1**(`goal.json` 单份)。 --- ## 8. 图 见同目录 `唤醒-统一唤醒出口-20260930-图.html`(① 统一后的流程:两个拨钟方 → 一个出口;② 判定三分图:`target-busy` ≠ 没有可唤醒对象)。