session-mechanism:修目标检查线静默死(P0-97)

病根:闸③ 的静默基准只按 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+留痕 / 还原→建排期 / 两路全空→三行留痕)
This commit is contained in:
admin committed 2026-10-12 23:42:16 +08:00
1 parent 20eacef289
commit 510ba9192c
3 files changed
+194 -59

No files matched your search

+33
View File
@@ -3761,3 +3761,36 @@ var r = el.getBoundingClientRect();
- **留痕口径**:本次**只加拒收闸**,既有坏行**不动**(用户选 B 案=台账留痕优先)。 - **留痕口径**:本次**只加拒收闸**,既有坏行**不动**(用户选 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)。
另一条:**判据的取值形态不要靠"我想到的几种写法拉倒"** —— 形态是别人写进来的,
能想到三种就会漏第四种,而漏掉的那种是**静默**的。要么归一(解码+规范),要么别用字符串匹配。
+156 -58
View File
@@ -4279,8 +4279,18 @@ _CHECK_TAIL_A = """
# ⚠️ 占位符第 3 位=**目标目录绝对路径**(由 `_check_prompt()` 传 `os.path.join(_g, goal_dir_rel())`)。 # ⚠️ 占位符第 3 位=**目标目录绝对路径**(由 `_check_prompt()` 传 `os.path.join(_g, goal_dir_rel())`)。
_CHECK_TAIL_B = """ _CHECK_TAIL_B = """
## %s、硬约束 ## %s、硬约束
- 🔴 **只建排期,不代替任务会话干活**;建完就停,⛔ 不要连建多条。 - 🔴 **派活这一步只建排期,不代替任务会话去干活**;建完就停,⛔ 不要连建多条。
- 🔴 建排期用 `automation_update` 工具,**`cwds` 必须逐字等于** `%s`(⛔ 错一字面即裂组)。 - 🔴🔴 **上面那句只圈「这条棒该干的活」,⛔ 圈不住收尾动作**(2026-10-12 修 · 实测病根:同一份派活单里
既写了「做完了 ⇒ 写回验收 + 改状态」,又写了「只建排期、不代替干活」⇒ 检查会话在冲突时选了后者,
把收口**当任务派给了 `[执行]` 前缀的棒**,而改状态的那条命令**只写在检查棒的职责里** ⇒ 必然漏,
目标于是挂在「进行中」上 21 小时)。
⇒ 要派给任务会话的是**实现目标的那件活**;⛔ **本文正文写明「你自己执行」的收尾动作一律由你亲自落笔**:
按正文对应章节回写验收判据、按正文给你的**那个**状态出口改目标状态(结果是**你判的**,
⛔ 派一条棒出去等于把判断权也交出去了)。⛔ 别再把「只建排期」当成不做收尾的依据。
- 🔴 建排期用 `automation_update` 工具,**`cwds` 必须逐字等于** `%s`
(**正斜杠形态**,⛔ 别把 Windows 的反斜杠形态发回去 —— 落库会变成转义形态,
与机制自建的那批**裂成两个组**,读取侧也可能认不出那是本工作区 ⇒ 判据静默失效,
2026-10-12 实测栽过)。
- 🔴🔴 **本目标有专属文件夹**:`%s` - 🔴🔴 **本目标有专属文件夹**:`%s`
- 派出去的棒**产物一律落这里**,⛔ **别再散到 `交付物/`、`docs/` 或工作区根目录**。 - 派出去的棒**产物一律落这里**,⛔ **别再散到 `交付物/`、`docs/` 或工作区根目录**。
- 理由:产物集中 ⇒ 检查会话照台账 `artifact` 就能核实,⛔ 不用到处找 - 理由:产物集中 ⇒ 检查会话照台账 `artifact` 就能核实,⛔ 不用到处找
@@ -4501,7 +4511,14 @@ def _check_prompt(n: int, goal_title: str, reason: str = "queue-empty") -> str:
# 尾节实参语义:`(节号, collabd.py, collabd.py, _g)`。 # 尾节实参语义:`(节号, collabd.py, collabd.py, _g)`。
# ⚠️ 2026-10-03 11:0x 尾节第 3 位=**目标目录**(派棒产物落点)。 # ⚠️ 2026-10-03 11:0x 尾节第 3 位=**目标目录**(派棒产物落点)。
# 语义:`(节号, collabd.py, collabd.py, 工作区, 目标目录)` # 语义:`(节号, 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`。 # 🔴🔴 2026-10-03 10:3x **第 2 项改指 `tasks.json`(唯一权威)**,⛔ 不再指 `queue.json`。
# 真因(实测):`queue.json` 是**已退役的旧投递机制**留下的文件,**已 12 小时无人写入**, # 真因(实测):`queue.json` 是**已退役的旧投递机制**留下的文件,**已 12 小时无人写入**,
# 停在过期的 `head=S12 blocked-by-owner`;而 `tasks.json` 里 S12 **早已 done**。 # 停在过期的 `head=S12 blocked-by-owner`;而 `tasks.json` 里 S12 **早已 done**。
@@ -4671,36 +4688,102 @@ def artifacts_unverifiable() -> list:
return out 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: def _last_progress_ts() -> float:
"""**最近一次有实质进展的时刻**(epoch 秒)—— 判「静默够久没」(阈值 `CHECK_IDLE_MIN`,现 3 分钟)的基准。 """**最近一次有实质进展的时刻**(epoch 秒)—— 判「静默够久没」(阈值 `CHECK_IDLE_MIN`,现 3 分钟)的基准。
🔴 取「本工作区排期的 `updated_at` 最大值」;读不到 ⇒ 判**刚有过活动**(0) 🔴🔴 **为什么不能只看排期表**(2026-10-12 实测坐实 —— 就是它让目标检查线**静默停了 21 小时**):
⇒ ⛔ 不建(fail-safe:宁可少建,不误判成"已静默够久")。 静默基准原本**只**取「本工作区排期的 `updated_at` 最大值」。那条路一旦判假(见 `_cwds_hit()`
🔴🔴 匹配必须**两种斜杠形态都试**(实测踩过):`cwds` 落库形态随写入方而异 的实测:`cwds` 转义形态撞不上),本函数就回 `0` ⇒ 闸③ 那句 `if not lp: return None`
(`automation_update` 写正斜杠、`WS` 变量可能是反斜杠)⇒ 只用一种必然匹配 0 行 **静默**返回 ⇒ 每 20 秒一次、零日志 ⇒ **「读不到判据」与「静默还不够久」在日志里长得一模一样**。
⇒ 静默时长恒为 0 ⇒ **那条闸永远不会开**(静默失效,不是误建)。 ⇒ 现在**两路取最大**:① 排期表的最近变更时刻(会话/机制在建排期);② 会话侧痕迹文件的 mtime。
任一路有读数就作数 ⇒ 排期表为空(目标已收口、排期全删)时**闸照样能开**。
⚠️ 两路**都**读不到 ⇒ 回 `0.0` 并**留一行限流日志**(⛔ 不再静默 —— 静默正是这次 21 小时
没人发现的直接原因)。
""" """
_w = str(WS or "") _w = str(WS or "")
if not _w: if not _w:
return 0.0 return 0.0
# ⚠️⛔ 拼接**必须用 f-string/+**,⛔ 不许用 `"%s%" % _w`: ts = 0.0
# Windows 路径里的 `\` 会被 `%` 当转义(`\%`/`\P`)⇒ 实测直接抛 # ① 排期表:⛔ 必须走 `_cwds_hit()` 这个唯一判定 —— 别再在本地拼 `like` 形态
# `ValueError: incomplete format`(第一次跑就踩,且异常被 except 吞成 0.0 静默失效)。 # (拼 LIKE 的老毛病:形态想到几种就漏几种,而漏掉的那种是**静默**的)。
cands = ("%" + _w + "%", try:
"%" + _w.replace("\\", "/") + "%", c = _ro_conn()
"%" + _w.replace("/", "\\") + "%") _rows = c.execute(
for pat in cands: "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: try:
c = _ro_conn() _p = Path(_w) / _rel
a = c.execute( if _p.is_file():
"select max(updated_at) from automations where cwds like ? " ts = max(ts, _p.stat().st_mtime)
"and deleted_at is null", (pat,)).fetchone()[0]
c.close()
if a:
return float(a) / 1000.0
except Exception: except Exception:
continue 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 " "where status='ACTIVE' and deleted_at is null "
"and (last_run_at is null or last_run_at=0)").fetchall() "and (last_run_at is null or last_run_at=0)").fetchall()
c.close() c.close()
pats = {_w, _w.replace("\\", "/"), _w.replace("/", "\\")}
# 🔴 过期宽限:宿主扫描周期 ≤30s、实测到点延迟 20~35s ⇒ **15 分钟**是安全余量。 # 🔴 过期宽限:宿主扫描周期 ≤30s、实测到点延迟 20~35s ⇒ **15 分钟**是安全余量。
# ⛔ 宽限期内**仍算待执行**(那正是"马上要开"的情形,闸④ 该拦)。 # ⛔ 宽限期内**仍算待执行**(那正是"马上要开"的情形,闸④ 该拦)。
_STALE_MS = 15 * 60 * 1000 _STALE_MS = 15 * 60 * 1000
_now_ms = int(time.time() * 1000) _now_ms = int(time.time() * 1000)
# 🔴🔴 2026-10-12 **匹配改用 `_cwds_hit()`(唯一判定)**,⛔ 不再在本地拼一组 `like` 形态。
# 病根(实测):`cwds` 是 JSON 文本、现网两种写法并存(`/` 形态与**转义**的 `\\` 形态)
# ⇒ 本地拼形态**想到几种就漏几种,而漏掉的那种是静默的** ⇒ 判据恒假。
_rec = [] # 本轮跳过的周期排期(只为记一条日志,⛔ 不进 out) _rec = [] # 本轮跳过的周期排期(只为记一条日志,⛔ 不进 out)
_stale = [] # 本轮跳过的"不会再触发"的一次性排期(只为记一条日志,⛔ 不进 out)
for _nm, _cd, _st, _na in rows: for _nm, _cd, _st, _na in rows:
_s = str(_cd or "").strip() if not _cwds_hit(_cd):
if not _s:
continue continue
if any(p and p.lower() in _s.lower() for p in pats): _nm = str(_nm or "").strip()
if _nm == exclude: 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 continue
# 🔴 一次性排期**已不会再触发** ⇒ 排除,否则它**永远**卡住闸④。 out.append(_nm)
# 两种"不会再触发"(2026-10-03 12:0x 实测,两种都踩过): if _stale:
# (a) `next_run_at` 已过宽限; log_throttled("gate4-stale-once", 600,
# (b) `next_run_at` 被**宿主清成 NULL** —— 实测排期到点触发后宿主会清它, "闸④:跳过 %d 条不会再触发的一次性排期(⛔ 否则它会永远卡住闸)|%s"
# 而 `status` 仍 `ACTIVE`、`last_run_at` 仍 `None` % (len(_stale), _stale[:3]))
# ⇒ 只判 (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))
if _rec: if _rec:
log("闸④:跳过周期排期(`recurring` 会反复触发 ⇒ 不算『有人正要干这件活』)|%s" log("闸④:跳过周期排期(`recurring` 会反复触发 ⇒ 不算『有人正要干这件活』)|%s"
% _rec[:3]) % _rec[:3])
@@ -4859,6 +4949,14 @@ def maybe_spawn_check_agent(reason: str) -> dict |None:
return None return None
lp = _last_progress_ts() lp = _last_progress_ts()
if not lp: 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# ⛔ 读不到进展时刻 ⇒ 不建 return None# ⛔ 读不到进展时刻 ⇒ 不建
idle_min = (time.time() - lp) / 60.0 idle_min = (time.time() - lp) / 60.0
if idle_min < CHECK_IDLE_MIN: if idle_min < CHECK_IDLE_MIN:
+5 -1
View File
@@ -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 null_excluded = "if str(_st or \"\").lower() == \"once\":" in _c and "_gone = (not _na)" in _c
# ⑥ 🔴 2026-10-12(用户选 A 案):`recurring` 一律排除 —— 周期排期永远有下一次, # ⑥ 🔴 2026-10-12(用户选 A 案):`recurring` 一律排除 —— 周期排期永远有下一次,
# 拿"在册+没跑过"判它恒真 ⇒ 闸④ 恒拦 ⇒ 目标检查会话一条都建不出来。 # 拿"在册+没跑过"判它恒真 ⇒ 闸④ 恒拦 ⇒ 目标检查会话一条都建不出来。
# ⚠️ 2026-10-12 同日**去掉了 `_rec.append(str(_nm))` 里的 `str(...)`**(`_nm` 上面已归一)
# ⇒ 判据跟着放宽成 `_rec.append(`(⛔ 不绑死在"参数怎么写"上;仍绑死"命中 recurring 就
# 记进 `_rec` 并 `continue`"这个**行为**)—— 同 P0-3 的教训:改判据里的字面量,
# 一定要 grep 一遍**所有**引用点,否则留下一条只会假红的死判据。
recurring_excluded = ( recurring_excluded = (
'if str(_st or "").lower() == "recurring":' in _c 'if str(_st or "").lower() == "recurring":' in _c
and "_rec.append(str(_nm))" in _c and "_rec.append(" in _c
and "continue" in _c) and "continue" in _c)
# ⑤ 真库现读:造一条过期 once 也不能把**别人**的排期算进来(⛔ 只验判据形状,不动生产) # ⑤ 真库现读:造一条过期 once 也不能把**别人**的排期算进来(⛔ 只验判据形状,不动生产)
live = m.ws_pending_schedules() live = m.ws_pending_schedules()