diff --git a/session-mechanism/references/pitfalls.md b/session-mechanism/references/pitfalls.md index 2aa6072..26653a8 100644 --- a/session-mechanism/references/pitfalls.md +++ b/session-mechanism/references/pitfalls.md @@ -2000,6 +2000,18 @@ INBOX = '…\ai1net-dsh-server\tmp\supervise-inbox' ⇒ 它会**永远**把闸④ 卡住 ⇒ 本工作区**再也建不出检查会话**(静默、无报错)。 ✅ 两种"不会再触发"都判:**(a)** `next_run_at` 已过 15 分钟宽限;**(b)** `next_run_at` 被清成 NULL。 ⚠️ **只对 `once` 生效**(`recurring` 永远会再触发,⛔ 不按过期排除)。 +- 🔴🔴 **2026-10-12 补:`recurring`(周期排期)整体不算"待执行"**(用户当日选 A 案,已改 `collabd.py`)。 + **症状**:周期排期**永远有下一次** ⇒ 拿"在册 + `last_run_at` 为空"去判它**恒真** ⇒ 闸④ **恒拦** + ⇒ **目标检查会话一条都建不出来**。 + **实测**(`aigc-idea-impression`,10-11 → 10-12):被「AI变现日报 · 每日自动生成」(`FREQ=DAILY;BYHOUR=6`) + 挡了整整一天,常驻日志每 20 秒重复一行「目标检查:⚠️ 本工作区还有 1 条待执行排期 ⇒ 不建」; + 后果是**执行轮有人干、核目标那一步没人干**。 + **为什么排除是合理的**:闸④ 要防的是「**马上要开工**时重复派活」;周期排期是按日历重复的, + **不表示有人正要干这件活** —— 它到点自己开一条会话,检查会话顶多多跑一轮,代价远小于"永远不核目标"。 + **边界**:只排除 `recurring`,`once` 的拦截照留;只动**本工作区**(`_same_ws` 已命中)⇒ 别的工作区不受影响。 + **自检**:`selftest.py` 用例「闸④:排除**已过期的一次性**排期」现为 **6 项**(新增第 ⑤ 项验 `recurring` 排除)。 + ⚠️ 变更前先跑**变异对照**:同一工作区、同一时刻,旧版 `ws_pending_schedules()` 返回 + `['AI变现日报 · 每日自动生成']`,新版返回 `[]` —— 证明是这次改动在起作用,不是"本来就空"。 - **实测取证(2026-10-05 只读)**:闸④ 对 `selftest` 工作区跳过 **11 条**、`vibe-product` 跳过 **1 条**, 全部是 `next_run_at=NULL(已消费)` 这一类 —— 正是 (b)。若不跳过,这两个工作区**各自永远卡死**。 - 🔴 **已知隐患(未修,待拍板)**:工作区判据是 **子串匹配** `p.lower() in _s.lower()`(第 ~4464 行), @@ -3726,3 +3738,26 @@ var r = el.getBoundingClientRect(); > **改判据的核心动作不是"换个更对的比法",是"再破坏一次看它会不会喊"。** > 变异对照不做 ⇒ 你的"修复"很可能只是把**假红**换成了**恒绿**。 + +--- + +## P0-96 🔴🔴 **漏写 `--report` 的需求 id ⇒ 静默往台账写一条坏记录,还会被算进队列计数**(★ 2026-10-12 实测,本区 + `vibe-product` 都中过) + +- **它长什么样**:收报写成 `--report --state done --artifact …`(**漏了 id**)—— + 看着像正常命令,实际 `_arg("--report")` 取到的是**下一个 token**,也就是 `--state`。 +- **后果(两层,第二层更狠)**: + ① 台账 `tasks.json` 里多出一条**键名写坏**的记录(键=`"--state"`)—— + 命令回显是「OK 已上报」,**零报错**; + ② 它**被算进队列计数**(本区实测 `queue_n` 2 → 3;`vibe-product` 的 `q_sig` 里也有一条 `--state=done`)。 + 若落成 `running`/`blocked` 态 ⇒ 闸① 误判「有活在跑」⇒ 去建结果检查会话 ⇒ 查一个不存在的东西。 +- **✅ 已修**(2026-10-12 用户选 B 案):`--report` 分支加**拒收闸** —— + id 为空或以 `--` 开头 ⇒ 打用法行 + **退出码 2**,台账**一个字都不写**。 +- **变异对照(做过的,不是推理)**:把拒收闸从副本里删掉 ⇒ 同一条命令 **rc=0** 且台账真写出 `"--state"`;加回去 ⇒ **rc=2** 且台账为空。 +- **判据自身的坑(两处,都会造成假绿)**: + ① **正例探针⛔不能指向真工作区** —— 我当场就是这么往真台账里敲进一条 `ZZZ-假id`(`running`)的,只能手工撤回; + ⇒ 一律 `COLLABD_WORKSPACE=<临时目录>` 起子进程。 + ② **临时工作区必须自带 `collabd.config.json`** —— 否则 `collabd.py` 先「未找到部署配置 ⇒ 拒跑」并返回 **rc=2** ⇒ + 负例断言**因为错的理由**变绿。⇒ 夹具现造最小配置,并加一条**正例**(形态正确的 id ⇒ rc=0)当对照。 +- **留痕口径**:本次**只加拒收闸**,既有坏行**不动**(用户选 B 案=台账留痕优先)。 +- 🔴 **写工具的红线**:**凡是"看着成功、其实写坏了"的通道,优先加"当场拒收"** —— + 报错比静默污染便宜得多;静默污染要等到有人去读那条记录才发现,而那时原因已经不在现场了。 diff --git a/session-mechanism/scripts/collabd.py b/session-mechanism/scripts/collabd.py index f896586..db9100c 100644 --- a/session-mechanism/scripts/collabd.py +++ b/session-mechanism/scripts/collabd.py @@ -4693,6 +4693,14 @@ def ws_pending_schedules(exclude: str = "") -> list: ② `cwds` 命中本工作区(⛔ 两种斜杠形态都试 —— 同 `_all_sessions_idle` 的教训: 只试一种 ⇒ 匹配 0 行 ⇒ **恒判"没排期"⇒ 闸失效**); ③ **还没跑过**(`last_run_at is null or last_run_at=0`)—— 跑过的一次性排期**不再算**。 + 🔴🔴 **2026-10-12 收窄:`recurring`(周期排期)一律不算"待执行"**(用户当日选 A 案)。 + · 症状:周期排期**永远有下一次** ⇒ 拿"在册 + 没跑过"去判它**恒真** ⇒ 闸④ 恒拦 + ⇒ **目标检查会话一条都建不出来**(本区 10-11 → 10-12 实测:被「AI变现日报 每日 06:00」 + 挡了整整一天,日志每 20 秒重复一行「还有 1 条待执行排期 ⇒ 不建」)。 + · 反例库(为什么可以排除):闸④ 要防的是「**马上要开工**时重复派活」—— + 周期排期是按日历重复的,**不表示有人正要干这件活**;它到点会自己开一条会话, + 检查会话顶多多跑一轮,代价比"永远不核目标"轻得多。 + · 边界:只排除本工作区(`_same_ws` 已在上面命中)⇒ 别的工作区不受影响。 ⚠️ `next_run_at` **故意不看**:⛔ 一条 `next_run_at` 已过期但仍 ACTIVE 的一次性排期 (实测本工作区有 3 条,其中 2 条 `next_run_at=None`)**仍然会被人点开**(`automation_update` 手动起) ⇒ 用 `next_run_at` 当"待执行"会漏。判据取「**在册 + 没跑过**」这个更宽的口径。 @@ -4716,6 +4724,7 @@ def ws_pending_schedules(exclude: str = "") -> list: # ⛔ 宽限期内**仍算待执行**(那正是"马上要开"的情形,闸④ 该拦)。 _STALE_MS = 15 * 60 * 1000 _now_ms = int(time.time() * 1000) + _rec = [] # 本轮跳过的周期排期(只为记一条日志,⛔ 不进 out) for _nm, _cd, _st, _na in rows: _s = str(_cd or "").strip() if not _s: @@ -4734,6 +4743,13 @@ def ws_pending_schedules(exclude: str = "") -> list: # 📌 风险登记:若存在"建了但 `next_run_at` 就是 NULL"的 `once` 排期,会被误排除; # 但 `create_check_schedule()` 的**硬规则第一条**就是 `next_run_at` 必须 # 是有限数值 ⇒ 机制自己建的排期不会命中该风险。 + # 🔴🔴 2026-10-12(用户选 A 案):**周期排期不算"待执行"** + # —— 它永远有下一次,拿"在册 + 没跑过"判它恒真 ⇒ 闸④ 恒拦 ⇒ + # 目标检查会话一条都建不出来(本区被「AI变现日报 每日 06:00」挡了一整天)。 + # 依据与反例库见函数 docstring;⛔ 只排除 `recurring`,`once` 的拦截照留。 + if str(_st or "").lower() == "recurring": + _rec.append(str(_nm)) + continue if str(_st or "").lower() == "once": _gone = (not _na) # (b) 已被消费 ⇒ 到点时间被清空 if not _gone: @@ -4746,6 +4762,9 @@ def ws_pending_schedules(exclude: str = "") -> list: % _nm) continue out.append(str(_nm)) + if _rec: + log("闸④:跳过周期排期(`recurring` 会反复触发 ⇒ 不算『有人正要干这件活』)|%s" + % _rec[:3]) except Exception as e: log("ws_pending_schedules 读库失败(⇒ 按有排期处理,不建) %s" % e) return ["<读不到排期表>"] @@ -6510,6 +6529,22 @@ def main() -> int: def _arg(k, dv=""): return sys.argv[sys.argv.index(k) + 1] if (k in sys.argv and sys.argv.index(k) + 1 < len(sys.argv)) else dv + # ── 🔴🔴 拒收闸(2026-10-12 用户选 B 案):`--report` 后面**必须跟一个真的需求 id** ── + # 症状(实测):写成 `--report --state done --artifact …`(**漏了 id**)⇒ + # `_arg("--report")` 取到的是**下一个 token**(实为 `--state`)⇒ + # 台账里多出一条**键名写坏**的记录,还会**被算进队列计数** + # (本区实测 `queue_n` 2 → 3;`vibe-product` 也有一条同型)。 + # ⇒ 这类污染是静默的:命令看着像成功、台账看着像有记录,事后极难发现。 + # ⇒ 处置=**当面报错 + 给用法行 + 退出码 2**,⛔ 不写台账。 + _rid = _arg("--report") + if (not _rid) or _rid.startswith("--"): + print("✗ `--report` 后面必须跟**需求 id**(漏写会往台账写一条键名写坏的记录," + "还会被算进队列计数)。\n" + " 用法:--report <需求id> --state pending|running|done|blocked " + "[--by 会话名] [--artifact 产物] [--reason 原因] [--line 线]\n" + " 实得:--report %s" % (_rid or "<空>")) + return 2 + rc = task_report(_arg("--report"), _arg("--state", "running"), _arg("--by"), _arg("--artifact"), _arg("--reason"), _arg("--line")) if rc == 0: # 上报**只写台账**(⛔ 不投递)—— diff --git a/session-mechanism/scripts/selftest.py b/session-mechanism/scripts/selftest.py index 3cb7ade..d66ae0b 100644 --- a/session-mechanism/scripts/selftest.py +++ b/session-mechanism/scripts/selftest.py @@ -3891,6 +3891,79 @@ def t_check_prompt_no_wild_hunt(): +@case("🔴 `--report` 拒收闸:漏写需求 id ⇒ 报错退出 + ⛔ 不写台账(2026-10-12 用户选 B 案)") +def t_report_requires_id(): + """🔴 2026-10-12 实测缺陷(本区 + `vibe-product` 都中过)。 + + 🔴 **现象**:把收报写成 `--report --state done --artifact …`(**漏了 id**)⇒ + `_arg("--report")` 取到的是**下一个 token**(实为 `--state`) + ⇒ 台账里多出一条**键名写坏**的记录,而且它**被算进队列计数** + (本区实测 `queue_n` 2 → 3;`vibe-product` 的 `q_sig` 里也有一条 `--state=done`)。 + 🔴 **为什么必须拦**:这类污染是**静默**的 —— 命令看着像成功、台账看着像有记录; + 更糟的是它可能落在 `running`/`blocked` 态 ⇒ 闸① 误判「有活在跑」⇒ 去建结果检查会话。 + ✅ **正解**=`--report` 分支加拒收闸:id 为空或**以 `--` 开头** ⇒ 打用法行 + 退出码 2, + 台账一个字都不写。 + + ⚠️ 判据自身的坑(两个都得堵,否则**假绿**): + ① **正例探针⛔绝不能指向真工作区** —— 我 2026-10-12 09:43 就是这么往真台账里 + 敲进一条 `ZZZ-假id`(`running`)的,只能手工撤回。⇒ 一律用临时工作区。 + ② **临时工作区必须自带 `collabd.config.json`** —— 不然 `collabd.py` 会先 + 「未找到部署配置 ⇒ 拒跑」并返回 **rc=2** ⇒ 负例断言**因为错的理由**变绿。 + ⇒ 夹具里现造一份最小配置,并加一条**正例**(形态正确的 id ⇒ rc=0)当对照。 + """ + import json as _j + import os as _os + import shutil as _sh + import subprocess as _sp + import tempfile as _tf + import pathlib as _pl + m = imp() + _src = (HERE / "collabd.py").read_text(encoding="utf-8") + _own = (HERE / "selftest.py").read_text(encoding="utf-8") + has_guard = ('if (not _rid) or _rid.startswith("--"):' in _src + and '✗ `--report` 后面必须跟' in _src) + uses_tmp = 'env["COLLABD_WORKSPACE"] = str(tb)' in _own + tb = _pl.Path(_tf.mkdtemp(prefix="dsh-report-id-")) + try: + cfg = tb / ".workbuddy" / "collab" / "collabd.config.json" + cfg.parent.mkdir(parents=True, exist_ok=True) + cfg.write_text(_j.dumps({"workspace": str(tb).replace("\\", "/"), + "inbox": "tmp/supervise-inbox", + "live": "交付物/实时状态.md", + "singleton_port": 20999}, ensure_ascii=False), + encoding="utf-8") + (tb / "y.md").write_text("夹具产物", encoding="utf-8") # ⛔ 别让 artifact 缺失来抢答 + env = dict(_os.environ) + env["COLLABD_WORKSPACE"] = str(tb) + env["COLLABD_CONFIG"] = str(cfg) + env["COLLABD_NO_ENSURE"] = "1" + _cmd = [sys.executable, str(HERE / "collabd.py"), "--report"] + # 正例(对照):形态正确的 id ⇒ 必须被受理,证明配置可用、确实走到了 --report 分支 + r_ok = _sp.run(_cmd + ["T-正常id", "--state", "running", "--by", "b"], + cwd=str(tb), env=env, capture_output=True, text=True, timeout=120) + # 负例:漏 id(=本轮真出事的写法) + r_bad = _sp.run(_cmd + ["--state", "done", "--by", "b", "--artifact", "y.md"], + cwd=str(tb), env=env, capture_output=True, text=True, timeout=120) + rc_ok, rc = r_ok.returncode, r_bad.returncode + out = (r_bad.stdout or "") + (r_bad.stderr or "") + said = ("必须跟" in out) and ("需求 id" in out) + led = list(tb.rglob("tasks.json")) + blob = "".join(p.read_text(encoding="utf-8", errors="replace") for p in led) + no_bad_key = '"--state"' not in blob + wrote_ok = '"T-正常id"' in blob + finally: + _sh.rmtree(str(tb), ignore_errors=True) + return [ + ("① 源码里有拒收闸(id 为空或以 `--` 开头 ⇒ 拒收)", has_guard), + ("② 正例对照:形态正确的 id ⇒ 受理(rc=%s;⚠️ 拿掉它,③ 就可能因" + "「未找到部署配置」而假绿)" % rc_ok, rc_ok == 0 and wrote_ok), + ("③ 漏写 id ⇒ **退出码 2**(实得 %s)" % rc, rc == 2), + ("④ 报错说清缺什么(含「需求 id」与正确用法)", said), + ("⑤ ⛔ 一个字都没写进台账(无坏键 `\"--state\"`)", no_bad_key), + ("⑥ 判据本身用**临时工作区**起子进程(变异:拿掉那行 ⇒ 本项变红)", uses_tmp), + ] + + @case("台账纪律:`done` 必须带产物文档 + `blocked` 必须带原因(否则检查会话只能去翻)") def t_done_requires_artifact(): """🔴 2026-10-03 10:5x(用户要求逐字): @@ -4355,11 +4428,14 @@ def t_gate4_stale_once(): ⇒ ⛔ 靠 `last_run_at` 收口**永远收不了**。 ✅ **正解**=排除「**已过期的一次性**排期」,宽限 **15 分钟** (宿主扫描周期 ≤30 s、实测到点延迟 20~35 s ⇒ 15 min 是安全余量)。 - ⚠️ 四条边界(缺一条就误杀或漏杀): + ⚠️ 五条边界(缺一条就误杀或漏杀): ① 刚过期 ⇒ **仍算待执行**(马上要开 ⇒ 闸④ 该拦) ② 过期 >15 min ⇒ **排除** ③ `next_run_at` 为 NULL ⇒ **仍算待执行**(⛔ 别误杀) - ④ `recurring` 过期 ⇒ **仍算待执行**(它会再触发) + ④ `recurring` ⇒ **一律排除**(2026-10-12 用户选 A 案) + 🔴 **2026-10-12 新增边界 ④(用户选 A 案)**:`recurring` 周期排期**不算"待执行"**。 + 症状:周期排期永远有下一次 ⇒ 老口径下闸④ **恒拦** ⇒ 本区目标检查会话**一条都建不出来** + (实测被「AI变现日报 每日 06:00」挡了一整天,日志每 20 秒重复「还有 1 条待执行排期 ⇒ 不建」)。 """ import re as _re import time as _tm @@ -4383,6 +4459,12 @@ def t_gate4_stale_once(): # ⇒ 我上一版写"NULL ⇒ 仍算待执行(别误杀)"**是错的** ⇒ 那样它照样永��卡闸④。 # (第五种情形:(a) 已过宽限 / (b) 已被消费 ⇒ 两者都排除。) null_excluded = "if str(_st or \"\").lower() == \"once\":" in _c and "_gone = (not _na)" in _c + # ⑥ 🔴 2026-10-12(用户选 A 案):`recurring` 一律排除 —— 周期排期永远有下一次, + # 拿"在册+没跑过"判它恒真 ⇒ 闸④ 恒拦 ⇒ 目标检查会话一条都建不出来。 + recurring_excluded = ( + 'if str(_st or "").lower() == "recurring":' in _c + and "_rec.append(str(_nm))" in _c + and "continue" in _c) # ⑤ 真库现读:造一条过期 once 也不能把**别人**的排期算进来(⛔ 只验判据形状,不动生产) live = m.ws_pending_schedules() return [ @@ -4391,7 +4473,8 @@ def t_gate4_stale_once(): % limit_min, 10 <= limit_min <= 30), ("③ 只排 `once`(⛔ `recurring` 会再触发,永不按过期排除)", only_once), ("④ `once` 且 `next_run_at` 为 NULL(已被宿主消费)⇒ **也排除**", null_excluded), - ("⑤ 判据现读不抛(ws_pending_schedules() = %s)" % (live if live else "[]"), True), + ("⑤ `recurring`(周期排期)**一律排除** ⇒ 闸④ 不再恒拦", recurring_excluded), + ("⑥ 判据现读不抛(ws_pending_schedules() = %s)" % (live if live else "[]"), True), ]