Files
dsh_ai1net_server/交付物/唤醒-双排期实现方式与状态动作表-20260930.md
admin c1b5e4d966 chore(工作区): 全量入库 + 补齐 .gitignore(以工作区为准)
- 变更规模:新增 514 / 修改 62 / 重命名 155 / 删除 4(归档重组与文档轮次)
- .gitignore 修:`归档/**/db-cwd归一-备份-*/` —— 原规则写绝对层级(归档/db-cwd归一-…),
  目录搬进 归档/配置与备份/ 后**静默失效**,43 MB 的 DB 备份又变成未跟踪
- .gitignore 补:嵌套 git 内部数据(归档/内嵌git-20261008/、归档/skills-git-旧线-20261007/dotgit-原样移出/)
- .gitignore 补:运行态与部署副本(.workbuddy/collab/、.workbuddy/tools/、.workbuddy/.load-pending、.workbuddy/tmp-*)
- .gitignore 补:备份件(*.bak-*)
- 未跟踪文件从 2190 降到 890(其余为 归档/ 归档件与 .workbuddy/memory/ 知识文件,按口径入库)
2026-10-10 23:13:22 +08:00

152 lines
9.2 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 唤醒 · 双排期实现方式 + 状态→动作表(2026-09-30 20:2x)
> 线:机制线 · 唤醒机制 | 域:`ai1net-dsh-server/`
> 上游:`交付物/唤醒-无人值守可行性-20260930.md`(判定「叫醒旧会话无路」)
> 配图:`交付物/唤醒-双排期实现方式与状态动作表-20260930-图.html`
---
## 0. 一句话
**唤醒 = 到点由宿主起一个「新会话」,它第 0 步跑状态、抢域锁,然后按【状态表】决定干什么。**
每个项目挂 **2 条每小时排期、相位错开 30 分钟** ⇒ 实际每 30 分钟有一次唤醒;**互斥靠同一条域锁**(抢不到就退),所以两条不会重复干活。
🔴 **这对排期不是常驻的** —— 它**从属于「目标」**:下口令启动,目标完成或有阻碍时一起暂停(§4)。
---
## 1. 四条硬事实(本机实测,⛔ 别重做)
| # | 事实 | 证据 |
|---|---|---|
| 1 | **相位 = 建立/启用那一刻** | `20:03:42` 建 ⇒ 下次 `21:03:42`;改 `rrule` 后 `20:04:03` ⇒ 下次 `21:04:03` |
| 2 | **只改提示词不重算相位** | 改完提示词后 `nextRunAt` 与改前**逐毫秒相同**(`…458318`) |
| 3 | **暂停时相位被清空** | `PAUSED` 的行 `next_run_at` = NULL(对照 `1eaf45c3`)⇒ 重新启用时**又会按"启用那一刻"重算** ⇒ **两条必须错开启用 30 分钟** |
| 4 | **分钟写不死**:`BYMINUTE` 被接受但**被忽略**;`BYHOUR` **列表**被直接拒绝("must be an integer") | 建 `FREQ=HOURLY;INTERVAL=1;BYMINUTE=30` ⇒ 仍按建立时刻触发 |
⇒ **"错开半小时"只能靠"两条相隔 30 分钟落地"得到,写不进规则里。**
---
## 2. 落地配方
### 2.1 建立(一次性,每个项目建一次)
| 步 | 动作 | 备注 |
|---|---|---|
| 1 | 建**唤醒轮 A、B**:`FREQ=HOURLY;INTERVAL=1`,`cwds` = 该项目(**逐字同形**:正斜杠),`status` 先给 **PAUSED** | 两条的提示词里写死**同一把域锁**+状态表(见 §5) |
| 2 | 把两条的 **id 写进彼此的提示词**(自我暂停要用) | 本项目 id 见下表 |
**本项目已建立**:
| 角色 | automation id | 当前 |
|---|---|---|
| 唤醒轮 A | `caca9a89-fa82-48e1-a55c-7e0f3a06bd48` | **PAUSED**(已建好,待口令启用) |
| 唤醒轮 B | `16bec5ce-98c0-470c-afa9-6f5a06218068` | **PAUSED**(同上) |
### 2.2 🔴 启动(每次下达「执行 XXX 目标」时,三件事一起做)
| 步 | 动作 | 为什么 |
|---|---|---|
| 1 | 把 **A 置 `ACTIVE`** | 它的相位 = 此刻 |
| 2 | 排**一条一次性排期**(+30 分钟)去把 **B 置 `ACTIVE`** | 这样 B 的相位 = A+30 分钟;**这就是"错开半小时"的唯一手段** |
| 3 | 用 `mode=list` **核对**两条 `nextRunAt` 相差 ≈30 分钟 | 不对 ⇒ 重复第 1、2 步(先暂停 A 再启用) |
⚠️ 那条 +30 分钟的启动棒是**一次性、用完即废**,⛔ 不要长期留着。
### 2.3 停止(目标完成 或 有阻碍)
把 **A、B 一起置 `PAUSED`**。两种触发都成立:
- **AI 自动**:唤醒轮自己判到"② 卡在用户"或"③ 目标已收口" ⇒ 它自己把两条暂停(§3);
- **人/主会话**:目标验收全过或决定作废 ⇒ 由收口的那个会话暂停。
⛔ **唤醒轮不许把自己从 PAUSED 启回来** —— 只有「执行 XXX 目标」这道口令能启。
---
## 3. 🔴【核心】状态 → 动作表
唤醒轮抢到锁之后,**只认这四种状态**;判不准一律按 ③ 处理。
| # | 状态 | 判据(怎么认) | 该干什么 | ⛔ 不许 |
|---|---|---|---|---|
| **①** | **有缺口** | 链断了/有**明确且已定死**的下一步(不是"应该再研究研究") | 写**一行**排期把下一棒接上(新棒 id **只从工具返回值取**)⇒ 回一句结论、结束 | 不许一次派多棒;不许派"再调研一下" |
| **②** | **卡在需要用户** | 缺凭据/缺真机/缺审批/有 `NEED-USER.md` 或 blocked 项 | ⛔ **不派棒**(派了=空转链条);把"要用户做什么"写成**可执行一句话**(要哪台机器、要哪句授权)报出来;**然后把 A、B 一起置 `PAUSED`**,结束 | 不许"受阻就硬派别的棒" |
| **③** | **无缺口/目标已收口** | 队列空/没有客观可判的下一步 | 一句话说明"无缺口",结束。**若确认"当前没有任何在途目标" ⇒ 把 A、B 一起置 `PAUSED`** | ⛔ 不造活;⛔ 不做巡检/体检/日报 |
| **④** | **判不准** | —— | 按 ③ 处理(宁可空转一轮) | 不许"先干点什么看看" |
**另外两条前置状态(抢锁之前就决定):**
| 状态 | 判据 | 动作 |
|---|---|---|
| **锁被占** | 抢域锁失败 | **立即结束**(有人正在推进)⛔ 不等待、⛔ 不接管、⛔ 不删别人的锁(R9) |
| **相位漂移** | 两条 `nextRunAt` 不再相差 ≈30 分钟,或停在过去 | 按 §2.2 重做启用;⚠️ 已见过 `004f6bc2` 的 `next_run_at` 停在 09-28 23:19 ⇒ **漂移真会发生** |
---
## 4. 目标生命周期 × 唤醒轮(🔴 2026-09-30 20:2x 用户定:「由执行 XXX 目标的口令同时启动,完成或有阻碍时同时暂停」)
| 目标处于 | 唤醒轮 A/B | 谁执行 |
|---|---|---|
| **下达「执行 XXX 目标」** | 一起**启动**(A 即刻 + 排一条 +30 分钟启 B ⇒ 相位错开 30 分钟) | 接口令的会话 |
| **在途(有缺口)** | 保持 ACTIVE;每 30 分钟一次唤醒,按 ① 接棒 | 唤醒轮自己 |
| **有阻碍(卡在用户)** | **一起暂停** + 报告"需要用户做什么" | **唤醒轮自己**(不必等人) |
| **完成(验收全过)/作废** | **一起暂停**(作废则 `delete` 两条) | 收口会话 |
| **用户放行/继续** | 重新下"执行 XXX 目标"口令 ⇒ 重新启用并校准相位 | 接口令的会话 |
⇒ **净效果**:只有"目标真的在途"时才烧唤醒轮;收口或受阻即刻停 ⇒ 空转轮总量可控(§6)。
---
## 5. 唤醒轮提示词(模板,已写进 A/B 两条)
```
你是本工作区(<WS>)的唤醒轮 <A|B>。本轮只做一件事,做完即停。
🔴 唤醒 = 起一个「新会话」(⛔ 不是叫醒旧会话 —— 旧会话的网关口随进程消失)
🔴 本排期从属于「目标」:只有"正在执行某个目标"期间才该 ACTIVE;完成或有阻碍 ⇒ 一起停
第 0 步(必做,先跑再说话)
1. 跑状态:<PY> <WS>/state.py
2. 抢域锁:handoff-guard.sh --claim-exec "唤醒轮<A|B>" --domains <项目>/
抢不到 ⇒ 立刻结束(天然去抖)。⛔ 不等待、⛔ 不接管、⛔ 不删别人的锁
抢到锁之后:按【状态表】动作
①有缺口→写一行排期接棒(id 只取返回值) ②卡在用户→⛔不派棒,只报告,然后把 A/B 一起置 PAUSED
③无缺口/已收口→⛔不造活,一句话结束;确认无在途目标则把 A/B 一起置 PAUSED ④判不准→按③
本对排期 id(自我暂停用;⛔ 只改 status,不动 rrule):A=<id_a> B=<id_b>
重启规则:只有「执行 XXX 目标」口令能启用,⛔ 自己不许把自己启回来
硬约束:⛔ 不重启宿主/服务|⛔ 不改全局 settings.json|⛔ 不起常驻长跑任务
|⛔ 不 kill 任何会话|⛔ 不投递消息给用户其它会话|收尾必须 --release-exec
```
---
## 6. 代价(如实登记,⛔ 不粉饰)
| 项 | 数 |
|---|---|
| **在途期间**频率 | 每项目 **2 次/小时** ⇒ 48 次/天 |
| **收口/受阻后** | **0 次**(一起暂停)⇒ 这是本方案相对"常驻排期"的关键优势 |
| 每次唤醒成本 | 一轮完整会话(哪怕③空转出口也要"跑状态+抢锁+回一句") |
| 粒度下限 | **30 分钟**(宿主只认 HOURLY/DAILY/WEEKLY/MONTHLY/YEARLY,5 分钟级做不到) |
| 与旧文档的冲突 | `~/.workbuddy/skills/multi-session-collab/references/architecture.md` §4 现写着「**⛔ 不引入任何周期排期**」(依据用户 09-30 原话「又给我整到自动任务去了」)。**本方案是用户 09-30 20:02 后的新指令,取代那一条** ⇒ 该文件 §4 需更正,⛔ 不改则文档与实现打架 |
---
## 7. 校准与撤销
- **查相位**:`automation_update mode=list` 读两条 `nextRunAt`(应相差 ≈30 分钟;PAUSED 时为空,属正常)。
- **校准**:按 §2.2 重做启用(先全暂停,再 A 即刻、B 隔 30 分钟);或对某一条 `delete` + `create` 重建。
- **撤销**:`mode=delete` 删两条 ⇒ 回到"只有收尾自判"的旧形态。⛔ 删锁/接管仍禁止(R9)。
⚠️ **删除是软删**:行仍留在 `automations` 表里、`status` 仍是 `ACTIVE`,只是写了 `deleted_at`(调度器按它排除)⇒ **判断"是否还在跑"要看 `deleted_at IS NULL`,⛔ 别只看 `status`**。
- ⚠️ **别用 SQL 直接改 `automations` 表**(宿主持有;一律走 automation_update)。
---
## 8. 出处
- 判定上游:`交付物/唤醒-无人值守可行性-20260930.md`(§3 源码判据 / §6 worker 存活时长 / §7 替代路径)
- 宿主表(只读):`E:/ProgramData/.workbuddy/workbuddy.db` → `automations`(`next_run_at`/`status`)/`automation_runs`(实际触发时刻)
- 技能:`~/.workbuddy/skills/multi-session-collab/`(§4 节奏章待更正)