修「检查会话落到未分组」+ 检查程序静默阈值 20 → 10 分钟

一、检查会话跑到未分组(用户报障逐字)
「vibe-product 的检查会话没有创建到工作区下,跑到未分组的会话中去了」。
根因(实测坐实):collabd.py::create_check_schedule() 的 INSERT **少了 workspace_scope 列**
⇒ 落库 NULL ⇒ 排期触发出来的会话被宿主当**游乐场**建
(sessions 行:is_playground=1 + source_mode='craft' + use_sandbox_cli=0)⇒ 不进工作区分组。
对照读数(同一晚两条排期):会话侧 automation_update 建的是 'workspace' ✓,
机制自建的是 None ❌;近 3 天 59 条排期里只有机制那条是 None。
修法:INSERT 补 workspace_scope 并以 'workspace' 落库。
配套:新增 selftest 用例 t_check_schedule_scope(3 项),含变异对照
(把列删掉 ⇒ 报红;还原 ⇒ 逐字节一致)。

二、检查程序等待时间 20 → 10 分钟(用户令逐字)
「把检查程序的等待时间由 20分钟 改为 10分钟」。
改法:新增常量 CHECK_IDLE_MIN = 10,**闸门与 --dry-run 试算都读它**(原来两处各写死 20);
并同步**写给检查会话看的 prompt 文案**(≥20 → ≥10)与 4 处注释/文档
(references/supervise-persistence.md §六;顺带订正该节「基准取台账 tasks.json 的 mtime」——
与代码不符,实际是「本工作区排期的 updated_at 最大值」)。
配套:新增 selftest 用例 t_check_idle_min(4 项,**不锁数值**、只锁"常量/闸门/试算/文案同源"),
含变异对照(把文案改回 20 ⇒ 报红)。

三、旧数据归位与分发
- 今晚那条歪掉的检查会话已改回工作区档(is_playground 1→0、source_mode craft→work、
  use_sandbox_cli 0→1;排期 workspace_scope → 'workspace');改前原值已备份到
  归档/检查会话归位-20261007/;PRAGMA integrity_check = ok。
- 副本分发:ai1net-dsh-server 与 vibe-product 各 7 个文件;两区常驻已重启
  (判据=心跳 started_h 07:54:35 / 07:54:37,晚于分发时刻 07:54:07)。

验收:selftest.py rc=0 PASS 102 / FAIL 0(改前 99);manifest 70 份、语法失败 0。
This commit is contained in:
admin committed 2026-10-07 07:55:29 +08:00
1 parent 9a665039a9
commit a3935b5fc5
4 files changed
+87 -20

No files matched your search

+4 -4
View File
@@ -1,6 +1,6 @@
# manifest · 包内文件清单
> 生成方式:逐文件 `compile()` / `json.loads` + md5 | **最近一次全量重算:2026-10-07 01:47 (派活触发句收工闸:触发句留待办 + Stop 拦一次(含新判据))**
> 生成方式:逐文件 `compile()` / `json.loads` + md5 | **最近一次全量重算:2026-10-07 07:55 (检查排期补 workspace_scope(修未分组)+ 静默阈值 20→10 + 两条新判据)**
> ⚠️ **2026-10-05 局部增量**:`references/pitfalls.md`(P0-77 拆条 + P0-73/P0-77 压缩)与
> `scripts/goalctl.py`(`--switch-goal` 确认闸 + 旧目标归档)两行的 md5/大小已按当天实测值更新;**其余行仍是 10-04 基线**。
> ⛔ 本表**不含** `install.log`(运行日志)与 `references/manifest.md`(自引用,写完即失真)。
@@ -139,7 +139,7 @@
| `references/karpathy-output-ladder/references/ste100.md` | 3022 | `fdbe3271c21a3c17f326d1e71de33b0d` | — |
| `references/pitfalls.md` | 311254 | `d321a71e38dd246b8014b4c9100b12c7` | — |
| `references/rules.md` | 8106 | `95af3566066d27c3c224ce681a82efc1` | — |
| `references/supervise-persistence.md` | 34394 | `95c5c7b75acf8c2240eec7d267a51ded` | — |
| `references/supervise-persistence.md` | 34783 | `3c68c146984133449b8bd6a1240909f1` | — |
| `references/taskgraph.md` | 3521 | `be6c6540be86475bb3430688c2987afc` | — |
| `references/作业规矩/00-作业总规矩(原 agent-operating-rules).md` | 70243 | `80fc5471015b237ee7aacd6976eda6fe` | — |
| `references/作业规矩/02-工作区纪律.md` | 21351 | `1a679fdeb9cca0883a47f619d1aec85d` | — |
@@ -152,7 +152,7 @@
| `scripts/board_ext.py` | 45655 | `2319fb9d2d21bf961e03a42da9a1ad4b` | ok |
| `scripts/collabctl.py` | 28124 | `827fe07da25b86b773a3f701e86db351` | ok |
| `scripts/collabd.config.example.json` | 1158 | `6655c15c411a84e3ca12fd136dbe2c60` | ok |
| `scripts/collabd.py` | 433099 | `f177baed038b7ef90ae17d0abd175399` | ok |
| `scripts/collabd.py` | 434710 | `00bded98f6c49fa30881434df168642e` | ok |
| `scripts/deliver-gateway-token.py` | 5685 | `cbba9648d404ecd2683061e19dbdb24a` | ok |
| `scripts/deploy_code.py` | 5325 | `a79cf5e5ffb3e8c60a31b4d0d8845f40` | ok |
| `scripts/forensics/proc-parent.py` | 2347 | `8cdfff2dbb88303298752c3777da7b20` | ok |
@@ -177,7 +177,7 @@
| `scripts/lock/op-lock.sh` | 4690 | `1a31eda3642e23693a65e1519860c744` | — |
| `scripts/lock/preflight-lock.sh` | 8673 | `e985ec1cb853fae4c351cd03b171355d` | — |
| `scripts/mut_run.py` | 13323 | `3bffc12e1e57d5efc743bac55e584b62` | ok |
| `scripts/selftest.py` | 484110 | `f9125fcc8ba2751c41f1fe77afd51739` | ok |
| `scripts/selftest.py` | 487051 | `8ef1fb1b33bf088fdae3dcef158011c9` | ok |
| `scripts/session-rules-check.py` | 44412 | `b2b468992d80421106316eaf451e2477` | ok |
| `scripts/stop-collab.py` | 10462 | `d8043bedd23b52b6642aaf7ec8b8c980` | ok |
| `scripts/supervise-launch.py` | 2503 | `f416a2f197e5524a8859c1d227ba1d05` | ok |
@@ -374,8 +374,11 @@ $p = Get-Process -Id <pid> -ErrorAction SilentlyContinue # $null = 已死
## 六、⚠️ 还没有它就不算长期(两条,别自欺)
1. **静默基准**:检查会话要 `静默 ≥ 20 分钟` 才建,而基准取台账 `tasks.json` 的 mtime
⇒ **新建/改动任何排期都会把计时归零** ⇒ 刚派完棒必然等满 20 分钟(实测 0.2/4.3/…/12.6 分钟,**从未越过 20**)。
1. **静默基准**:检查会话要 `静默 ≥ 10 分钟` 才建(🔴 **2026-10-07 由 20 改 10**,用户原话
「把检查程序的等待时间由 20分钟 改为 10分钟」;阈值=`collabd.py::CHECK_IDLE_MIN`,闸门与 `--dry-run` 试算同源)。
基准=**本工作区排期的 `updated_at` 最大值**(`_last_progress_ts()`);
⚠️ 本行原先写「基准取台账 `tasks.json` 的 mtime」——**那句与代码不符**,已按代码订正。
⇒ **新建/改动任何排期都会把计时归零** ⇒ 刚派完棒必须再等满阈值(实测 0.2/4.3/…/12.6 分钟,**在旧阈值 20 下从未越过**)。
2. **闸二会自锁(正确行为,不是 bug)**:`_all_sessions_idle()` 要求"所有会话都结束",
而**发起这一切的那条会话自己也在里面** ⇒ 只要它活着,检查会话就建不出来。
⇒ **要让检查会话跑起来,必须先结束发起会话**(fail-safe 方向=宁可不建,不误建)。
+27 -13
View File
@@ -2067,6 +2067,12 @@ TASK_EVENTS = INBOX / "tasks-events.jsonl" # 只作审计(append-only,⛔
TO_MAIN = INBOX / "TO-MAIN.md" # 投递给主会话的通知(投影 · 供人/AI 直接读)
TASK_STATES = ("pending", "running", "done", "blocked") # 需求台账四态:待执行/执行中/已完成/有阻碍
QUEUE_IDLE_MIN = 5 # 唤醒**限流桶**(分钟):同一状态下最多每 **5** 分钟发一次(2026-09-30 用户拍板:30→10→**5**);⛔ 触发条件是 supervise() 的**四条件**(不是计时器)
CHECK_IDLE_MIN = 10 # 🔴🔴 2026-10-07 用户拍板:**20 → 10 分钟**(原话「把检查程序的等待时间由 20分钟 改为 10分钟」)
# 「检查程序」判"已经静默够久、可以建检查会话"的阈值(`queue-empty` 那条闸)。
# ⚠️ 改这个数必须**一起改三处**,否则文案与判据打架:
# ① 本常量(闸门 `maybe_spawn_check_agent()` + `--dry-run` 试算都读它);
# ② `CHECK_PROMPT_GOAL` 里写给检查会话看的「已静默 ≥N 分钟」——那是**字面量**,⛔ 不会自动跟;
# ③ 文档 `references/supervise-persistence.md` §六(同样写着 20)。
HEARTBEAT_MIN_IDLE = 15 # 🔴 2026-09-30 加:**「真停滞」门槛**(分钟)——
# 距**上次任何进展** ≥ 15 分钟才允许发唤醒;**有进展 ⇒ 一条都不发**。
# 为什么:唤醒原只要求"三条件成立",而"目标未完成"在长任务期长期成立
@@ -4064,6 +4070,13 @@ def create_check_schedule(name: str, prompt: str, delay_s: int = 90,
# 落库若用反斜杠 ⇒ 与 automation_update 建的排期**裂成两个组**,看板上就是"裂组")
# 🔴🔴 2026-10-05 **P0-80**:模型⛔ 不许写死 —— 跟**本区主会话**取同一档(模型+思考档一起)。
_model, _thinking = preferred_model(ws)
# 🔴🔴 2026-10-07 **本 INSERT 必须写 `workspace_scope='workspace'`**(实测坐实,⛔ 少一列就裂组):
# 原来没有这一列 ⇒ 落库是 NULL ⇒ 排期触发出来的会话被宿主当成**游乐场**建
# (`sessions` 行:`is_playground=1` + `source_mode='craft'` + `use_sandbox_cli=0`)
# ⇒ **不进工作区分组**。用户报障原话:「**vibe-product 的检查会话没有创建到工作区下,
# 跑到未分组的会话中去了**」。
# 对照读数(同一晚两条排期):会话侧 `automation_update` 建的是 `'workspace'` ✓,
# 本机制自建的是 `None` ❌;近 3 天 59 条排期里**只有机制自建的那一条**是 None。
c = sqlite3.connect(_wb_db(), timeout=15)
try:
c.execute("PRAGMA busy_timeout=8000")
@@ -4071,12 +4084,12 @@ def create_check_schedule(name: str, prompt: str, delay_s: int = 90,
"""INSERT INTO automations
(id,name,prompt,status,schedule_type,scheduled_at,next_run_at,rrule,cwds,
created_at,updated_at,model_id,model_is_thinking,skills_json,
connector_ids_json,owner_user_id,owner_status)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
connector_ids_json,owner_user_id,owner_status,workspace_scope)
VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)""",
(aid, name, prompt, "ACTIVE", "once",
fire.strftime("%Y-%m-%dT%H:%M:%S"), now_ms + delay_s * 1000, "",
json.dumps([ws], ensure_ascii=False), now_ms, now_ms,
_model, _thinking, "[]", "[]", local_user_id(), "confirmed"),
_model, _thinking, "[]", "[]", local_user_id(), "confirmed", "workspace"),
)
c.commit()
finally:
@@ -4175,7 +4188,7 @@ def _all_sessions_idle() -> bool:
# 用户原话:「这里面是两件事 提示词应该不一样
# 1、所有会话都结束(还要加上 定时任务中没有本项目待执行的任务)且队列不为空时:
# 创建**结果检查会话**去跟进 协作会话执行结果,判断是否创建对应协作会话继续完成目标,并创建对应会话
# 2、所有会话都结束(还要加上 定时任务中没有本项目待执行的任务)且队列为空>20分钟时:
# 2、所有会话都结束(还要加上 定时任务中没有本项目待执行的任务)且队列为空>10分钟时:
# 创建**目标检查会话**去跟进 目标完成状态,判断是否创建对应协作会话继续完成目标 或 修改目标状态,并执行对应操作」
# ⛔ **不是同一件事的两种触发** ⇒ ⛔ 不许共用模板(共用=两个会话拿到互相串味的指令)。
#
@@ -4303,14 +4316,14 @@ CHECK_PROMPT_RESULT = (
+ _CHECK_TAIL_A + _CHECK_TAIL_B
)
# 【目标检查】—— 队列**空**且静默 >20 分钟时:核对目标完成状态
# 【目标检查】—— 队列**空**且静默 >10 分钟时:核对目标完成状态(阈值=`CHECK_IDLE_MIN`,2026-10-07 由 20 改 10)
CHECK_PROMPT_GOAL = (
"**【目标检查会话 · 第 %d 棒】**本轮只做这一件事:**核对目标完成状态**,**做完即停**。\n"
"\n"
"目标:%s\n"
"\n"
"## 一、为什么是你(🔴 触发条件已满足,不必重判)\n"
"**所有会话都已结束** + **队列为空** + **已静默 ≥20 分钟** + **本项目没有别的待执行排期**。\n"
"**所有会话都已结束** + **队列为空** + **已静默 ≥10 分钟** + **本项目没有别的待执行排期**。\n"
"\n"
"## 二、🔴 先读这五样,⛔ **只读这五个文件**,读完直接判断(⛔ 别到处找信息)\n"
"> ⚠️ **不要在本工作区里到处翻文件**:2026-10-03 实测,检查会话因无边界地找资料\n"
@@ -4556,13 +4569,13 @@ def artifacts_unverifiable() -> list:
def _last_progress_ts() -> float:
"""**最近一次有实质进展的时刻**(epoch 秒)—— 判「静默 20 分钟」的基准。
"""**最近一次有实质进展的时刻**(epoch 秒)—— 判「静默够久没」(阈值 `CHECK_IDLE_MIN`,现 10 分钟)的基准。
🔴 取「本工作区排期的 `updated_at` 最大值」;读不到 ⇒ 判**刚有过活动**(0)
⇒ ⛔ 不建(fail-safe:宁可少建,不误判成"静默 20 分钟")。
⇒ ⛔ 不建(fail-safe:宁可少建,不误判成"已静默够久")。
🔴🔴 匹配必须**两种斜杠形态都试**(实测踩过):`cwds` 落库形态随写入方而异
(`automation_update` 写正斜杠、`WS` 变量可能是反斜杠)⇒ 只用一种必然匹配 0 行
⇒ 静默时长恒为 0⇒ **「静默 20 分钟」这条闸永远不会开**(静默失效,不是误建)。
⇒ 静默时长恒为 0 ⇒ **那条闸永远不会开**(静默失效,不是误建)。
"""
_w = str(WS or "")
if not _w:
@@ -4710,8 +4723,9 @@ def maybe_spawn_check_agent(reason: str) -> dict |None:
if not lp:
return None# ⛔ 读不到进展时刻 ⇒ 不建
idle_min = (time.time() - lp) / 60.0
if idle_min < 20:
log("目标检查:队列空但只静默了 %.1f 分钟(<20)⇒ 不建" % idle_min)
if idle_min < CHECK_IDLE_MIN:
log("目标检查:队列空但只静默了 %.1f 分钟(<%d)⇒ 不建"
% (idle_min, CHECK_IDLE_MIN))
return None
else:
# 🔴 `sessions-ended`:队列**必须非空**(用户第①条「且队列不为空时」)
@@ -6360,13 +6374,13 @@ def main() -> int:
if _why_now == "sessions-ended":
_will = pend > 0
else:
_will = (life == GOAL_LIFE_RUN and pend <= 0 and idle >= 20)
_will = (life == GOAL_LIFE_RUN and pend <= 0 and idle >= CHECK_IDLE_MIN)
print(" ⇒ 会建:%s(按 reason=%s 判)" % ("是" if _will else "否", _why_now))
return 0
r = maybe_spawn_check_agent(why or "queue-empty")
print("已建检查会话排期:%s(%s 触发,%s 到点)" % (
r["name"], r["reason"] if "reason" in r else why or "queue-empty", r["fire_at"])
if r else "未建(闸门未过:状态非进行中 / 还有会话在跑 / 未静默 20 分钟 / 同名在册)")
if r else "未建(闸门未过:状态非进行中 / 还有会话在跑 / 未静默 %d 分钟 / 同名在册)" % CHECK_IDLE_MIN)
return 0
return 0
# 🔴🔴 **找不到使用方的部署配置 ⇒ 拒跑(fail-closed)**
+51 -1
View File
@@ -797,7 +797,7 @@ def t_two_kinds_of_check_agent():
用户原话两条:
1、会话全结束(**还要加上 定时任务中没有本项目待执行的任务**)且**队列不为空** ⇒
**结果检查会话**去跟进任务会话执行结果,判断是否创建对应任务会话继续完成目标;
2、会话全结束(同上)且**队列为空 >20 分钟** ⇒ **目标检查会话**去核对目标完成状态,
2、会话全结束(同上)且**队列为空 >10 分钟** ⇒ **目标检查会话**去核对目标完成状态,
判断是建任务会话还是**修改目标状态**,并执行。
⇒ 改前**两套 prompt 完全共用一个模板** ⇒ 结果检查会话拿到了"改目标状态"的出口(越权),
@@ -7295,5 +7295,55 @@ def t_dispatch_gate():
return r
@case("⛔ 机制自建的检查排期必须挂进工作区(少 workspace_scope ⇒ 会话落到未分组)")
def t_check_schedule_scope():
"""🔴 2026-10-07 立。
用户报障逐字:「**vibe-product 的检查会话没有创建到工作区下,跑到未分组的会话中去了**」。
实测根因:`collabd.py::create_check_schedule()` 的 INSERT **少了 `workspace_scope` 列**
⇒ 落库 NULL ⇒ 触发出来的会话被宿主当游乐场建(`is_playground=1` / `source_mode='craft'`
/ `use_sandbox_cli=0`)⇒ 不进工作区分组。
对照读数(同一晚两条排期):会话侧 `automation_update` 建的是 `'workspace'` ✓,
机制自建的是 `None` ❌;近 3 天 59 条排期里**只有机制那条**是 None。
"""
src = (HERE / "collabd.py").read_text(encoding="utf-8", errors="replace")
i = src.find("INSERT INTO automations")
seg = src[i:i + 1000] if i > 0 else ""
return [
("找得到建排期的那处 INSERT", i > 0),
("列清单里有 `workspace_scope`(⛔ 少了它,会话就落到未分组)",
"workspace_scope" in seg),
("且写死 `workspace`(⛔ 不是 NULL、也不是别的档)",
('"workspace"' in seg) or ("'workspace'" in seg)),
]
@case("⛔ 检查程序静默阈值:常量、闸门、试算、文案必须同源(⛔ 别只改一处)")
def t_check_idle_min():
"""🔴 2026-10-07 立(用户拍板:检查程序等待时间 **20 → 10 分钟**)。
这个数**散落在四处**,改一处漏三处就会"判据说 A、写给检查会话的文案说 B":
① 常量 `CHECK_IDLE_MIN`;② 闸门 `if idle_min < CHECK_IDLE_MIN`;
③ `--dry-run` 试算(⛔ 不同源 ⇒ 试算说"不会建"、实际却建了);④ `CHECK_PROMPT_GOAL` 的文案。
⚠️ 本条**不锁具体数值**(换个值不该报红)—— 只锁"四处同源"。
"""
import re as _re
src = (HERE / "collabd.py").read_text(encoding="utf-8", errors="replace")
m = _re.search(r"^CHECK_IDLE_MIN = (\d+)", src, _re.M)
v = int(m.group(1)) if m else -1
# ⚠️ 锚**定义处**(`= (`),⛔ 不能用 `find("CHECK_PROMPT_GOAL")` ——
# 它先命中本常量上面那条注释 ⇒ 3000 字窗口够不到真正的文案 ⇒ **假红**(第一版就这么栽的)。
i = src.find("CHECK_PROMPT_GOAL = (")
prompt = src[i:i + 3000] if i > 0 else ""
return [
("常量 `CHECK_IDLE_MIN` 存在且是整数(现为 %d 分钟)" % v, v > 0),
("闸门读常量、⛔ 不写死数字", "idle_min < CHECK_IDLE_MIN" in src),
("`--dry-run` 试算读同一个常量(⛔ 不同源 ⇒ 试算与实际打架)",
"idle >= CHECK_IDLE_MIN" in src),
("写给检查会话的文案与常量同值(⛔ 别只改判据、文案还写旧值)",
("≥%d 分钟" % v) in prompt),
]
if __name__ == "__main__":
sys.exit(main())