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:
1 parent
20eacef289
commit
510ba9192c
3 files changed
+194
-59
No files matched your search
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in new issue
Block a user