From 510ba9192cd58083a53e87abf01680f0ef07ecf1 Mon Sep 17 00:00:00 2001 From: maogeigei Date: Mon, 12 Oct 2026 23:42:16 +0800 Subject: [PATCH] =?UTF-8?q?session-mechanism=EF=BC=9A=E4=BF=AE=E7=9B=AE?= =?UTF-8?q?=E6=A0=87=E6=A3=80=E6=9F=A5=E7=BA=BF=E9=9D=99=E9=BB=98=E6=AD=BB?= =?UTF-8?q?=EF=BC=88P0-97=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 病根:闸③ 的静默基准只按 cwds 的两种斜杠形态做 like,而会话自建的排期落库是 转义形态(["E://ProgramData//…"])⇒ 匹配恒假 ⇒ _last_progress_ts() 回 0 ⇒ `if not lp: return None` 静默返回 ⇒ 本区目标检查线 21 小时零日志零告警 (03:00–22:59 每小时 359/360 行日志是"读任务图失败",闸门日志 0 行)。 修: - 新增 _cwds_hit():JSON 解码 + 路径归一的唯一判定(闸④ 与闸③ 共用) - _last_progress_ts() 两路取最大:排期表 + 会话侧痕迹文件;两路全空留限流日志 - 闸③ 静默 return 前补留痕;闸④ 逐条 log 改限流汇总(一夜 2.7 万行刷屏) - 派活单 cwds 改正斜杠形态(⛔ 不再制造转义行) - 检查棒尾节:收口动作必须自己落笔,⛔ 不许派给执行棒(且不越权写「已完成」) - pitfalls 新增 P0-97;selftest 闸④ 判据随变量名放宽(⛔ 不绑死写法) 验证:自检 PASS 110 / FAIL 0;变异对照三档(打死→None+留痕 / 还原→建排期 / 两路全空→三行留痕) --- session-mechanism/references/pitfalls.md | 33 ++++ session-mechanism/scripts/collabd.py | 214 +++++++++++++++++------ session-mechanism/scripts/selftest.py | 6 +- 3 files changed, 194 insertions(+), 59 deletions(-) diff --git a/session-mechanism/references/pitfalls.md b/session-mechanism/references/pitfalls.md index 26653a8..900124e 100644 --- a/session-mechanism/references/pitfalls.md +++ b/session-mechanism/references/pitfalls.md @@ -3761,3 +3761,36 @@ var r = el.getBoundingClientRect(); - **留痕口径**:本次**只加拒收闸**,既有坏行**不动**(用户选 B 案=台账留痕优先)。 - 🔴 **写工具的红线**:**凡是"看着成功、其实写坏了"的通道,优先加"当场拒收"** —— 报错比静默污染便宜得多;静默污染要等到有人去读那条记录才发现,而那时原因已经不在现场了。 + +--- + +## P0-97 🔴🔴 **一条 `like` 匹配不上 ⇒ 目标检查线静默死了 21 小时(零日志、零告警、外表全绿)**(★ 2026-10-12 本区实测) + +- **它长什么样**:闸③ 的静默基准 `_last_progress_ts()` 只做一件事 —— + 取「本工作区排期的 `updated_at` 最大值」,判据是 + `cwds like '%E:\ProgramData\…%'`(外加正斜杠形态)。 +- **那一行为什么撞不上**:`automations.cwds` 是 **JSON 数组文本**,现网**两种写法并存**: + · 机制自建(`create_check_schedule()`,明确归一过):`["E:/ProgramData/…"]` + · **会话自建**(`automation_update`,派活单把 `WS` 的**反斜杠原文**发给会话):`["E:\\ProgramData\\…"]` + ⇒ 单反斜杠/正斜杠两种 `like` **都撞不上转义出来的双反斜杠** ⇒ `max(updated_at)` 为 `None`。 +- **后果链(这才是要命的地方)**:`_last_progress_ts()` 回 `0` ⇒ 闸③ 那句 + `if not lp: return None` **静默**返回 ⇒ 每 20 秒一次、**一行日志都不写** + ⇒ 「读不到判据」与「静默还不够久」在日志里**长得一模一样** ⇒ + **目标检查线 21 小时一条棒都没建,没有任何告警**。 + 实测读数:`03:00–22:59` 每小时 360 行日志里 **359 行是「读任务图失败」**,闸门日志 **0 行**。 +- **触发它的小动作**:02:24 把本区旧排期整批标删 ⇒ 表里只剩**一条**本区排期, + 而它恰好是会话用反斜杠建的 ⇒ 判据从"能读"变成"恒读不到",**没有任何提示**。 +- **✅ 已修(三处,缺一不可)**: + ① 新增 `_cwds_hit()` —— **JSON 解码 + 路径归一后比**,成为唯一判定, + `_last_progress_ts()`/`ws_pending_schedules()` 共用(⛔ 不许在调用方再拼一组 `like`); + ② `_last_progress_ts()` **两路取最大**:排期表 + **会话侧痕迹文件**(`goal.json`/`tasks.json`/目标执行状态文档) + ⇒ 排期表为空也能开闸;两路全空 ⇒ **留一行限流日志**(⛔ 不再静默); + ③ 闸③ 那句 `return None` 前面补 `log_throttled`,**把"为什么不开闸"写进日志**。 + 另:派活单里的 `cwds` 改成正斜杠形态(与会话用同一形态,⛔ 不再制造转义行)。 +- **变异对照(做过的,不是推理)**:把 `_last_progress_ts()` 打死成 `0.0` ⇒ 闸③ 返 `None` + + 打出留痕行;还原 ⇒ **当场建排期**;再把"排期表 + 痕迹文件"两路一起弄瞎 ⇒ 返 `None` + 打三行留痕 + (库读失败/两路全空/闸③ 不建)。⇒ 判据既不是恒绿,也不是恒红。 +- 🔴 **通则**:**凡是 fail-safe 的"静默 return",都必须留一行限流日志** —— + 静默的 fail-safe 与"正常的等待"在日志里不可区分,等于把排查线索整条抹掉(同族:P0-52)。 + 另一条:**判据的取值形态不要靠"我想到的几种写法拉倒"** —— 形态是别人写进来的, + 能想到三种就会漏第四种,而漏掉的那种是**静默**的。要么归一(解码+规范),要么别用字符串匹配。 diff --git a/session-mechanism/scripts/collabd.py b/session-mechanism/scripts/collabd.py index 110341f..14c9d30 100644 --- a/session-mechanism/scripts/collabd.py +++ b/session-mechanism/scripts/collabd.py @@ -4279,8 +4279,18 @@ _CHECK_TAIL_A = """ # ⚠️ 占位符第 3 位=**目标目录绝对路径**(由 `_check_prompt()` 传 `os.path.join(_g, goal_dir_rel())`)。 _CHECK_TAIL_B = """ ## %s、硬约束 -- 🔴 **只建排期,不代替任务会话干活**;建完就停,⛔ 不要连建多条。 -- 🔴 建排期用 `automation_update` 工具,**`cwds` 必须逐字等于** `%s`(⛔ 错一字面即裂组)。 +- 🔴 **派活这一步只建排期,不代替任务会话去干活**;建完就停,⛔ 不要连建多条。 +- 🔴🔴 **上面那句只圈「这条棒该干的活」,⛔ 圈不住收尾动作**(2026-10-12 修 · 实测病根:同一份派活单里 + 既写了「做完了 ⇒ 写回验收 + 改状态」,又写了「只建排期、不代替干活」⇒ 检查会话在冲突时选了后者, + 把收口**当任务派给了 `[执行]` 前缀的棒**,而改状态的那条命令**只写在检查棒的职责里** ⇒ 必然漏, + 目标于是挂在「进行中」上 21 小时)。 + ⇒ 要派给任务会话的是**实现目标的那件活**;⛔ **本文正文写明「你自己执行」的收尾动作一律由你亲自落笔**: + 按正文对应章节回写验收判据、按正文给你的**那个**状态出口改目标状态(结果是**你判的**, + ⛔ 派一条棒出去等于把判断权也交出去了)。⛔ 别再把「只建排期」当成不做收尾的依据。 +- 🔴 建排期用 `automation_update` 工具,**`cwds` 必须逐字等于** `%s` + (**正斜杠形态**,⛔ 别把 Windows 的反斜杠形态发回去 —— 落库会变成转义形态, + 与机制自建的那批**裂成两个组**,读取侧也可能认不出那是本工作区 ⇒ 判据静默失效, + 2026-10-12 实测栽过)。 - 🔴🔴 **本目标有专属文件夹**:`%s` - 派出去的棒**产物一律落这里**,⛔ **别再散到 `交付物/`、`docs/` 或工作区根目录**。 - 理由:产物集中 ⇒ 检查会话照台账 `artifact` 就能核实,⛔ 不用到处找 @@ -4501,7 +4511,14 @@ def _check_prompt(n: int, goal_title: str, reason: str = "queue-empty") -> str: # 尾节实参语义:`(节号, collabd.py, collabd.py, _g)`。 # ⚠️ 2026-10-03 11:0x 尾节第 3 位=**目标目录**(派棒产物落点)。 # 语义:`(节号, collabd.py, collabd.py, 工作区, 目标目录)` - _tail = ("四", _me, _me, _g, os.path.join(_g, goal_dir_rel())) + _tail = ("四", _me, _me, _g.replace("\\", "/"), os.path.join(_g, goal_dir_rel())) + # 🔴🔴 2026-10-12 尾节第 2 位(派活单里的 `cwds`)**改成正斜杠形态**。 + # 依据(实测坐实 · 同一条病根的另一半):机制自建排期时**明确归一成正斜杠** + # (`create_check_schedule()` 里那句「落库若用反斜杠 ⇒ 与 `automation_update` 建的排期 + # 裂成两个组」),而**派活单**却把 `WS` 的**反斜杠**原文发给会话 + # ⇒ 会话照着建 ⇒ 落库是**转义形态** `["E:\\ProgramData\\…"]` + # ⇒ ① 与机制自建的那批**裂组**;② 读取侧旧实现拿单反斜杠 `like` **撞不上**(静默失效)。 + # ⇒ 派活单与会话用**同一形态**:正斜杠。 # 🔴🔴 2026-10-03 10:3x **第 2 项改指 `tasks.json`(唯一权威)**,⛔ 不再指 `queue.json`。 # 真因(实测):`queue.json` 是**已退役的旧投递机制**留下的文件,**已 12 小时无人写入**, # 停在过期的 `head=S12 blocked-by-owner`;而 `tasks.json` 里 S12 **早已 done**。 @@ -4671,36 +4688,102 @@ def artifacts_unverifiable() -> list: return out +def _norm_path(p) -> str: + """路径**只为比较**而归一:`\\`→`/`、去首尾引号与空白、去尾斜杠、转小写。 + + ⛔ **只用于比较**,⛔ 不用于落盘(落盘一律用 `WS` 原文)。 + """ + return str(p or "").replace("\\", "/").strip().strip('"').strip("'").strip().rstrip("/").lower() + + +def _cwds_hit(cwds_raw) -> bool: + """🔴🔴 **这条排期挂的是不是本工作区** —— 全脚本**唯一**判定实现(`_last_progress_ts()`/ + `ws_pending_schedules()` 共用,⛔ 不许在调用方再拼一份 `like`)。 + + 🔴🔴 **为什么必须「JSON 解码 + 路径归一」,⛔ 不能用文本 `like`**(2026-10-12 实测坐实; + 就是它把**目标检查线静默停了 21 小时**,且外表零异常): + `automations.cwds` 是 **JSON 数组文本**,现网**两种写法同时存在**: + · 机制自建(`create_check_schedule()`):`["E:/ProgramData/AIProject/contentm_agent"]` + · 会话自建(`automation_update` 工具传 Windows 路径):`["E:\\ProgramData\\AIProject\\contentm_agent"]` + —— 落库文本里是**转义形态**(双反斜杠)。 + ⇒ 旧写法拿 `like` 去撞**单反斜杠**形态 ⇒ **两种都撞不上**;改用 `/` 形态 + 同样撞不上转义出来的 `\\` ⇒ **判据恒假**(不是"少几条")。 + ⇒ 后果链:`max(updated_at)` 为 `None` ⇒ `_last_progress_ts()` 回 `0` ⇒ 闸③ 静默 `return None` + ⇒ 检查线每 20 秒空转一次、**零日志**(实测 03:00–22:59 每小时 360 行日志里 + 359 行是"读任务图失败",**没有任何闸门日志**,检查棒一条都没建)。 + ⚠️ 数组**可能不止一条路径**(多元素)⇒ 逐个比,命中任一即真。 + ⛔ 解析不出来 ⇒ 退回「整串当一条路径」比一次(⛔ 不直接判假 —— 那会把真命中丢成假)。 + """ + _want = _norm_path(WS) + if not _want: + return False + try: + _arr = json.loads(str(cwds_raw or "")) + except Exception: + _arr = None + _items = _arr if isinstance(_arr, list) else [str(cwds_raw or "")] + for _it in _items: + if isinstance(_it, dict): + _it = _it.get("cwd") or _it.get("path") or "" + if _norm_path(_it) == _want: + return True + return False + + +# 🔴 **会话侧**才会写的痕迹文件(判"静默够久没"的兜底依据)。 +# ⛔ **绝不把机制自己每轮都写的文件放进来**(`logs/_collabd.log`/心跳/`digest.md`/`advance.md`) +# —— 那是**自指型判据**:机制自己一动就把"静默"清零 ⇒ 那条闸永远不开(同族已栽过)。 +_PROGRESS_TRACE_RELS = ("tmp/supervise-inbox/goal.json", + "tmp/supervise-inbox/tasks.json") + + def _last_progress_ts() -> float: """**最近一次有实质进展的时刻**(epoch 秒)—— 判「静默够久没」(阈值 `CHECK_IDLE_MIN`,现 3 分钟)的基准。 - 🔴 取「本工作区排期的 `updated_at` 最大值」;读不到 ⇒ 判**刚有过活动**(0) - ⇒ ⛔ 不建(fail-safe:宁可少建,不误判成"已静默够久")。 - 🔴🔴 匹配必须**两种斜杠形态都试**(实测踩过):`cwds` 落库形态随写入方而异 - (`automation_update` 写正斜杠、`WS` 变量可能是反斜杠)⇒ 只用一种必然匹配 0 行 - ⇒ 静默时长恒为 0 ⇒ **那条闸永远不会开**(静默失效,不是误建)。 + 🔴🔴 **为什么不能只看排期表**(2026-10-12 实测坐实 —— 就是它让目标检查线**静默停了 21 小时**): + 静默基准原本**只**取「本工作区排期的 `updated_at` 最大值」。那条路一旦判假(见 `_cwds_hit()` + 的实测:`cwds` 转义形态撞不上),本函数就回 `0` ⇒ 闸③ 那句 `if not lp: return None` + **静默**返回 ⇒ 每 20 秒一次、零日志 ⇒ **「读不到判据」与「静默还不够久」在日志里长得一模一样**。 + ⇒ 现在**两路取最大**:① 排期表的最近变更时刻(会话/机制在建排期);② 会话侧痕迹文件的 mtime。 + 任一路有读数就作数 ⇒ 排期表为空(目标已收口、排期全删)时**闸照样能开**。 + ⚠️ 两路**都**读不到 ⇒ 回 `0.0` 并**留一行限流日志**(⛔ 不再静默 —— 静默正是这次 21 小时 + 没人发现的直接原因)。 """ _w = str(WS or "") if not _w: return 0.0 - # ⚠️⛔ 拼接**必须用 f-string/+**,⛔ 不许用 `"%s%" % _w`: - # Windows 路径里的 `\` 会被 `%` 当转义(`\%`/`\P`)⇒ 实测直接抛 - # `ValueError: incomplete format`(第一次跑就踩,且异常被 except 吞成 0.0 静默失效)。 - cands = ("%" + _w + "%", - "%" + _w.replace("\\", "/") + "%", - "%" + _w.replace("/", "\\") + "%") - for pat in cands: + ts = 0.0 + # ① 排期表:⛔ 必须走 `_cwds_hit()` 这个唯一判定 —— 别再在本地拼 `like` 形态 + # (拼 LIKE 的老毛病:形态想到几种就漏几种,而漏掉的那种是**静默**的)。 + try: + c = _ro_conn() + _rows = c.execute( + "select cwds, updated_at from automations where deleted_at is null").fetchall() + c.close() + for _cd, _up in _rows: + if _up and _cwds_hit(_cd): + ts = max(ts, float(_up) / 1000.0) + except Exception as _e: + log_throttled("progress-ts-db", 900, + "读排期表失败(⇒ 静默基准改用会话侧痕迹文件):%s" % _e) + # ② 会话侧痕迹文件(目标/台账/目标执行状态文档) + _rels = list(_PROGRESS_TRACE_RELS) + try: + _rels.append(exec_doc_rel()) + except Exception: + pass + for _rel in _rels: try: - c = _ro_conn() - a = c.execute( - "select max(updated_at) from automations where cwds like ? " - "and deleted_at is null", (pat,)).fetchone()[0] - c.close() - if a: - return float(a) / 1000.0 + _p = Path(_w) / _rel + if _p.is_file(): + ts = max(ts, _p.stat().st_mtime) except Exception: continue - return 0.0 + if ts <= 0: + # 🔴 留痕(限流):这一行就是"闸③ 到底为什么不开"的答案。 + log_throttled("progress-ts-none", 900, + "⚠️ 最近进展时刻读不到(排期表与会话侧痕迹**两路全空**)⇒ 闸③ 会一直不建检查棒") + return ts @@ -4746,49 +4829,56 @@ def ws_pending_schedules(exclude: str = "") -> list: "where status='ACTIVE' and deleted_at is null " "and (last_run_at is null or last_run_at=0)").fetchall() c.close() - pats = {_w, _w.replace("\\", "/"), _w.replace("/", "\\")} # 🔴 过期宽限:宿主扫描周期 ≤30s、实测到点延迟 20~35s ⇒ **15 分钟**是安全余量。 # ⛔ 宽限期内**仍算待执行**(那正是"马上要开"的情形,闸④ 该拦)。 _STALE_MS = 15 * 60 * 1000 _now_ms = int(time.time() * 1000) + # 🔴🔴 2026-10-12 **匹配改用 `_cwds_hit()`(唯一判定)**,⛔ 不再在本地拼一组 `like` 形态。 + # 病根(实测):`cwds` 是 JSON 文本、现网两种写法并存(`/` 形态与**转义**的 `\\` 形态) + # ⇒ 本地拼形态**想到几种就漏几种,而漏掉的那种是静默的** ⇒ 判据恒假。 _rec = [] # 本轮跳过的周期排期(只为记一条日志,⛔ 不进 out) + _stale = [] # 本轮跳过的"不会再触发"的一次性排期(只为记一条日志,⛔ 不进 out) for _nm, _cd, _st, _na in rows: - _s = str(_cd or "").strip() - if not _s: + if not _cwds_hit(_cd): continue - if any(p and p.lower() in _s.lower() for p in pats): - if _nm == exclude: + _nm = str(_nm or "").strip() + if _nm == exclude: + continue + # 🔴 一次性排期**已不会再触发** ⇒ 排除,否则它**永远**卡住闸④。 + # 两种"不会再触发"(2026-10-03 12:0x 实测,两种都踩过): + # (a) `next_run_at` 已过宽限; + # (b) `next_run_at` 被**宿主清成 NULL** —— 实测排期到点触发后宿主会清它, + # 而 `status` 仍 `ACTIVE`、`last_run_at` 仍 `None` + # ⇒ 只判 (a) 会漏掉这一类 ⇒ 它照样永远卡闸。 + # ⚠️ 只动 `once`:`recurring` 永远会再触发 ⇒ ⛔ 不按过期排除。 + # ⚠️ 只动**本工作区**(`_cwds_hit` 已在上面命中)⇒ ⛔ 不影响别的工作区。 + # 📌 风险登记:若存在"建了但 `next_run_at` 就是 NULL"的 `once` 排期,会被误排除; + # 但 `create_check_schedule()` 的**硬规则第一条**就是 `next_run_at` 必须 + # 是有限数值 ⇒ 机制自己建的排期不会命中该风险。 + # 🔴🔴 2026-10-12:**周期排期不算"待执行"** + # —— 它永远有下一次,拿"在册 + 没跑过"判它恒真 ⇒ 闸④ 恒拦 ⇒ + # 目标检查会话一条都建不出来(本区被「AI变现日报 每日 06:00」挡了一整天)。 + # 依据与反例库见函数 docstring;⛔ 只排除 `recurring`,`once` 的拦截照留。 + if str(_st or "").lower() == "recurring": + _rec.append(_nm) + continue + if str(_st or "").lower() == "once": + _gone = (not _na) # (b) 已被消费 ⇒ 到点时间被清空 + if not _gone: + try: + _gone = int(_na) + _STALE_MS < _now_ms # (a) 已过宽限 + except Exception: + _gone = False + if _gone: + # 🔴 2026-10-12:**逐条 `log()` 改限流汇总**(实测这条一夜刷了 2.7 万行, + # 把真事件全淹了 ⇒ 排查时"日志里啥也看不出来")。留痕照旧,只是不再刷屏。 + _stale.append(_nm) continue - # 🔴 一次性排期**已不会再触发** ⇒ 排除,否则它**永远**卡住闸④。 - # 两种"不会再触发"(2026-10-03 12:0x 实测,两种都踩过): - # (a) `next_run_at` 已过宽限; - # (b) `next_run_at` 被**宿主清成 NULL** —— 实测排期到点触发后宿主会清它, - # 而 `status` 仍 `ACTIVE`、`last_run_at` 仍 `None` - # ⇒ 只判 (a) 会漏掉这一类 ⇒ 它照样永��卡闸。 - # ⚠️ 只动 `once`:`recurring` 永远会再触发 ⇒ ⛔ 不按过期排除。 - # ⚠️ 只动**本工作区**(`_same_ws` 已在上面命中)⇒ ⛔ 不影响别的工作区。 - # 📌 风险登记:若存在"建了但 `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: - try: - _gone = int(_na) + _STALE_MS < _now_ms # (a) 已过宽限 - except Exception: - _gone = False - if _gone: - log("闸④:跳过不会再触发的一次性排期(⛔ 否则它会永远卡住闸)|%s" - % _nm) - continue - out.append(str(_nm)) + out.append(_nm) + if _stale: + log_throttled("gate4-stale-once", 600, + "闸④:跳过 %d 条不会再触发的一次性排期(⛔ 否则它会永远卡住闸)|%s" + % (len(_stale), _stale[:3])) if _rec: log("闸④:跳过周期排期(`recurring` 会反复触发 ⇒ 不算『有人正要干这件活』)|%s" % _rec[:3]) @@ -4859,6 +4949,14 @@ def maybe_spawn_check_agent(reason: str) -> dict |None: return None lp = _last_progress_ts() if not lp: + # 🔴🔴 2026-10-12 **这句必须留痕**(⛔ 静默 = 检查线死了没人知道)。 + # 实测病根:排期 `cwds` 是转义形态 ⇒ 旧的 `like` 形态全落空 ⇒ `_last_progress_ts()` + # 回 0 ⇒ 这里静默 `return None` ⇒ 每 20 秒空转、**零日志** + # ⇒ 目标检查线静默停了 21 小时(03:00–22:59 每小时 360 行日志里 359 行是 + # "读任务图失败",**没有任何闸门日志**,一条检查棒都没建)。 + log_throttled("check-no-progress", 600, + "目标检查:⛔ 读不到「最近进展时刻」⇒ 本轮不建" + "(留痕用:⛔ 否则它和『静默还不够久』在日志里长得一模一样)") return None# ⛔ 读不到进展时刻 ⇒ 不建 idle_min = (time.time() - lp) / 60.0 if idle_min < CHECK_IDLE_MIN: diff --git a/session-mechanism/scripts/selftest.py b/session-mechanism/scripts/selftest.py index d66ae0b..b0e1a44 100644 --- a/session-mechanism/scripts/selftest.py +++ b/session-mechanism/scripts/selftest.py @@ -4461,9 +4461,13 @@ def t_gate4_stale_once(): null_excluded = "if str(_st or \"\").lower() == \"once\":" in _c and "_gone = (not _na)" in _c # ⑥ 🔴 2026-10-12(用户选 A 案):`recurring` 一律排除 —— 周期排期永远有下一次, # 拿"在册+没跑过"判它恒真 ⇒ 闸④ 恒拦 ⇒ 目标检查会话一条都建不出来。 + # ⚠️ 2026-10-12 同日**去掉了 `_rec.append(str(_nm))` 里的 `str(...)`**(`_nm` 上面已归一) + # ⇒ 判据跟着放宽成 `_rec.append(`(⛔ 不绑死在"参数怎么写"上;仍绑死"命中 recurring 就 + # 记进 `_rec` 并 `continue`"这个**行为**)—— 同 P0-3 的教训:改判据里的字面量, + # 一定要 grep 一遍**所有**引用点,否则留下一条只会假红的死判据。 recurring_excluded = ( 'if str(_st or "").lower() == "recurring":' in _c - and "_rec.append(str(_nm))" in _c + and "_rec.append(" in _c and "continue" in _c) # ⑤ 真库现读:造一条过期 once 也不能把**别人**的排期算进来(⛔ 只验判据形状,不动生产) live = m.ws_pending_schedules()