Files
MailUI4Agents/deploy/recount-relay-counts.sh
JianFeeeee 2c34d6e368 fix(deploy): 占位行判据改用**行的年龄**(阈值 1 会把健康并发判成泄漏)+ 后置验证补 binrev==HEAD
两件都来自 pi 2026-09-25 的实测,我逐条复跑确认。

## ① `占位行 <= 1` 是阈值版假红(pi 127746b5,我复现)

`ClaimRelay`(INSERT 占位) 与 `BindRelayMail`(UPDATE 绑定) **不在一个事务里** ⇒
"同时有 2 个请求在飞"的正常形态**就是** 2 个占位行(t1 claim、t2 claim、t1 bind、t2 bind)。

    变异(/tmp/pl/t.db:清掉化石 + 插 2 个**合法在飞**占位)
      旧判据: PLACE=2 ⇒ rc=**1**  [FAIL] 占位行 <= 1     ← 把健康并发判成泄漏
      新判据: rc=**0**,打印"最老 = 0 分钟;其中 >10 分钟 = 0"

阈值 1 是"只有 1 个请求在飞的那天"的快照,与它要防的病(泄漏)**不正交**。

⇒ 与泄漏正交的判别量是**行的年龄**(pi 给的):正常占位行只活一个请求的时长,
  泄漏行活到**下一次请求之后**。年龄与并发度无关,也与"此刻是否恰好有请求"无关。
  阈值 10 分钟:远大于任何单请求(HTTP 有超时),远小于真实泄漏尺度(那行活了 12.7 天)。
  断言**上界**而非"==0"—— 0 是"恰好没请求在飞",不是健康的条件。

正反两面都验(只验一面会放过退化):
    健康并发 2 行 → rc=0 ✓      13 天泄漏 + 1 健康行 → rc=**1** ✓
  真库 → rc=1(那行确实 18259 分钟 ⇒ **真阳性**,非假红)
  并打印年龄 ⇒ 顺带答出"泄漏了多久"(旧判据只说"多了一行")。

## ② 抑制数丢了 keying(pi dd579a8e,我复核确认)

生成器键是 `(ag, prefix(k), r)`(**已含 agent**),但**序列化/打印时把 agent 丢了** ⇒
任何人引用这个数都会以为是"仅根"口径。实测两种 keying 全局差 6 封(61/37 vs 55/43,
6 组**全是跨 agent 合并**)⇒ 名字里不写,下一个人**必重犯**。

    文本: `抑制数 [keying=(agent,根) scope=全局]: …`
    JSON: `suppress_full_groups_keying_agent_root_scope_global`
  ★ 同时补 **scope**:本脚本报的是全局;session 侧同 keying 是另一个数(4/18)——
    同 keying 两个 scope 两个数,少写任一个都只能靠猜。
  ★ 只改名字,**数的值不动**(这是防再犯,不是纠错)。

## ③ 后置验证补 `binrev == HEAD`(pi 127746b5 §一,落在脚本而非只落检查器)

§7 那几项(服务 active / `/health` 200 / 无 panic / SSE 重连)**全都与代码版本无关** ⇒
**旧二进制跑这套清单会全绿**。2026-09-25 实证:线上是 09-19 的二进制而 §7 全过。

pi 建议落在 `redeploy-gateway.sh`,我采纳 —— 因为我昨天已把同一件事实现为
`check-deploy-drift` 判据 ⑤b,但**那个检查器没有任何自动入口**(要人手跑);
落在部署脚本里才能"装完当场就判"。

三态与 ⑤b 同口径:相等→绿 / 不等→**红**(就地回滚)/ 取不到任一侧→**不判**且必须说出来
(判红会让 `-buildvcs=false`、老构建、非 git 构建恒红)。

    实测逻辑: 线上 e8b260dd vs HEAD ⇒ 红 ✓;/nonexistent/binary ⇒ warn 不判 ✓

## 验证

`bash -n` 两文件通过;`recount-relay-counts.sh --json` 合法(23 键);
`criteria-hygiene.test.mjs` 7/7 通过(改名后仍满足 check-*.sh 命名约定)。
2026-09-25 06:27:09 +08:00

289 lines
18 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env bash
# 复算本会话里反复被引用的那几个 `relayed_mails` 数(**只读**,不写库)。
#
# # 为什么要这条
#
# 这条链上我们为同一批数**对了好几轮账**,每次都靠人在对话里重敲查询:
#
# · "summary 是 419 还是 422" —— pi 报 422,dsh 报 419;pi 后来承认报错,
# 但**双方都无法出示当时那条查询**,只能靠"再读五次"来对齐。
# · "抑制 37 还是 62" —— dsh 报 36/62、pi 报 61/37;
# 后来发现是**口径不同**(沿完整 parent 链 vs 只沿 failure 链),
# 而两个口径**都没写在信里**,所以先吵了两轮才找到差别。
# · "类 458 怎么来的" —— 判据 `kind <> 'failure'` 是**空真**
# (kind 只有 permission/summary),真正给 458 的是 relay_key 的形状。
#
# 三次的共同点**不是"谁算错了"**,而是 —— **结论落了盘,产生结论的查询没落盘**。
# docs/API.md 里留着"458""61/37"这些数,却没有任何一条能整段重跑的命令;
# 每个复算的人(包括我自己下一轮)都要**从头重写**查询,于是必然再分叉一次。
#
# ⇒ 本脚本把那几条查询**固定成命令**:谁不同意某个数,跑它、看输出。
#
# # 断言的是**不变量与关系**,不是冻结的数
#
# 库是**活**的(`relayed_mails` 随流量增减),所以分两层:
# · **不变量 / 关系** ⇒ 断言(不随后续流量变,只在"被数的东西变了"时红)
# · **当下读数** ⇒ 只**打印**(附时刻),不当断言 —— 否则明天必然假红
# 这正是本会话学到的另一条: 把**瞬时量**当断言,等于给自己埋一个定时假警报。
#
# # 口径声明(本脚本存在的主要理由)
# · `已绑定` = mail_id IS NOT NULL;`占位` = mail_id IS NULL
# · `类` 的判据是 `relay_key` 的**形状**,不是 `kind`(kind 里没有 failure)
# · 抑制数给**两种口径**(完整 parent 链 / 只沿 failure 链),因为两者差很多
#
# 用法: deploy/recount-relay-counts.sh [--json]
# 退出码: 0 = 全部**不变量**成立;1 = 有不变量不成立
#
# # ★ 为什么叫 `recount-` 而不是 `check-`(这条是我自己踩出来的)
#
# 本仓 `deploy/check-*.sh` 是**部署门禁族**的命名约定,且由
# `client/electron/test/criteria-hygiene.test.mjs` 用
# `readdirSync(DEPLOY).filter(/^check-.*\.sh$/)` **强制**:凡叫 check-*.sh 的,
# 必须被 install.sh / redeploy-*.sh 在**可执行位置**调用("判据在但走不到"是它要拦的形状)。
#
# 我这个脚本**第一版就叫 check-relay-counts.sh** ⇒ 被那条判据收进门禁族 ⇒ **把它弄红了**。
# 而按性质它**不该被接线**,两条入口接进去都是错的:
# · 接 `install.sh` ⇒ 它读 `/opt/agentmail/data/agentmail.db`,**新机器没有这个库** ⇒
# 实测 `AGENTMAIL_DB=/nonexistent` 时 rc=1 ⇒ **每次全新安装假红**;
# · 接 `redeploy-*` ⇒ 它断言的是"库的不变量",而**库是活的**,
# 计数一变就红 ⇒ 会去**回滚一次正确的部署**。
# ⇒ 所以正确修法是**改名**(进入"按需工具"族:`prune-*` / `archive-*` / `reset-*` 同族),
# 而不是改判据、也不是硬接线。
#
# ★ 顺带暴露了那条判据的一格: 它的**自称**按性质分("宣称是政策/门禁的 check-*.sh;
# 不管 check-deploy-drift.mjs 这类**按需手动工具**"),而**机制**按**扩展名**分。
# 两者在"按需工具 + .sh 后缀"这个缝里不一致 —— 我的文件正落在缝里。
# 反方向也有(真门禁 `check-plugin-snapshot.mjs` 因 .mjs **逃过**该判据)。
# ⇒ 这是 pi 那条⑬"判据的动作与它被许诺的范围不匹配"的又一实例。
set -euo pipefail
DB="${AGENTMAIL_DB:-/opt/agentmail/data/agentmail.db}"
JSON=0
[ "${1:-}" = "--json" ] && JSON=1
if [ ! -r "$DB" ]; then
printf ' [FAIL] 读不到库: %s\n' "$DB" >&2
exit 1
fi
q() { sqlite3 "file:${DB}?mode=ro" "$1" 2>/dev/null; }
# ── 当下读数(只打印,不断言)─────────────────────────────────────────
TOTAL=$(q 'select count(*) from relayed_mails;')
BOUND=$(q 'select count(*) from relayed_mails where mail_id is not null;')
PLACE=$(q 'select count(*) from relayed_mails where mail_id is null;')
# 注意: SQLite 的 group_concat(DISTINCT x, sep) **不合法**(DISTINCT 聚合只能一个参数)
KINDS=$(q "select group_concat(k,'|') from (select distinct kind as k from relayed_mails);")
KFAIL=$(q "select count(*) from relayed_mails where kind='failure';")
KIND_BAD=$(q "select count(*) from relayed_mails where kind not in ('permission','summary');")
NAIVE=$(q "select count(*) from relayed_mails where mail_id is not null and kind<>'failure';")
REAL=$(q "select count(*) from relayed_mails where mail_id is not null and (kind='permission' or relay_key not like '%failure%');")
SUBSTR_N=$(q "select count(*) from relayed_mails where mail_id is not null and relay_key like '%failure%';")
PREFIX_N=$(q "select count(*) from relayed_mails where mail_id is not null and (relay_key like 'model-failure%' or relay_key like 'service-failure%' or relay_key like 'zcode-failure%');")
HOMEAGENT=$(q "select count(*) from relayed_mails where mail_id is not null and relay_key like 'homeagent:failure:%';")
RESIDUE=$(q "select count(*) from relayed_mails where relay_key like 'no-such-session%' or relay_key like '%toolu-nohuman%';")
RESIDUE_UNBOUND=$(q "select count(*) from relayed_mails where mail_id is null and (relay_key like 'no-such-session%' or relay_key like '%toolu-nohuman%');")
# ★ 锚定不变量(不随流量变,且**有代码依据**,不是"恰好为 0"):
# permission 行的键由 `<sessionId>:<tool_use_id>` 拼(zcode hooks/permission.mjs:191),
# 而 `failure` 标记由**摘要桥**写给 summary 行(dsh-mail-bridge/src/index.ts:1240 等)。
# 两条写路径**不同** ⇒ permission 行里不该出现 failure 字样。
# ⇒ 这条是用来接住"计数关系全都没变、但语义已经错"的那类变异:
# 把一行 permission 的键改成 failure 形状时,substr 与 prefix 会同增 ⇒ 关系断言仍绿。
PERM_FAIL=$(q "select count(*) from relayed_mails where kind='permission' and relay_key like '%failure%';")
SUMMARY_FAIL=$(q "select count(*) from relayed_mails where kind='summary' and relay_key like '%failure%';")
# ★ 跨表 provenance(最强的一类:锚定到**别的表里的真实引用**,不是字符串形状):
# `kind='permission'` 的行来自 `CreatePermissionMail`,那封邮件**必然**在
# `permission_requests` 里有一行(permission.go 里同事务写)。反过来,摘要转发的邮件
# 不该在 `permission_requests` 里出现。
# ⇒ 这条能接住"只改 kind/键、计数关系全不变"的变异(⑤b):
# 那种变异会让一行变成"kind=summary 但仍有 permission_requests" ⇒ 反向断言变红。
PERM_NO_REQ=$(q "select count(*) from relayed_mails r where r.kind='permission' and r.mail_id is not null
and not exists (select 1 from permission_requests p where p.mail_id = r.mail_id);")
SUM_WITH_REQ=$(q "select count(*) from relayed_mails r where r.kind='summary' and r.mail_id is not null
and exists (select 1 from permission_requests p where p.mail_id = r.mail_id);")
# ── 抑制数(两口径,Python 因需递推上溯)─────────────────────────────
SUPP=$(python3 - "$DB" <<'PY'
import sqlite3, sys, collections
db = sys.argv[1]
c = sqlite3.connect('file:%s?mode=ro' % db, uri=True)
rows = c.execute("""select r.agent_name, r.relay_key, r.mail_id
from relayed_mails r
where r.mail_id is not null and r.relay_key like '%failure:%'""").fetchall()
par = {m: p for m, p in c.execute("select mail_id, parent_mail_id from mails")}
fail_ids = {mid for _, _, mid in rows}
def prefix(k):
i = k.find('failure')
return k[:i + len('failure')] if i >= 0 else k
def root_full(mid, cap=300):
seen, st, last = set(), 0, mid
while mid is not None and st < cap:
if mid in seen:
return 'LOOP'
seen.add(mid); last = mid; st += 1; mid = par.get(mid)
return 'CAP' if st >= cap else last
def root_fail(mid, cap=50):
seen, st, last = set(), 0, mid
while mid is not None and st < cap:
if mid in seen:
return 'LOOP'
seen.add(mid); last = mid; st += 1
nxt = par.get(mid)
if nxt is not None and nxt not in fail_ids:
break
mid = nxt
return 'CAP' if st >= cap else last
out = []
loops = []
for label, rf in (('full', root_full), ('fail', root_fail)):
g = collections.defaultdict(list)
nloop = 0
for ag, k, mid in rows:
r = rf(mid)
if r in ('LOOP', 'CAP'):
nloop += 1
g[(ag, prefix(k), r)].append(mid)
out.append('%s:%d/%d' % (label, len(g), sum(len(v) - 1 for v in g.values())))
loops.append('%s:%d' % (label, nloop))
print(' '.join(out) + ' loops ' + ' '.join(loops))
PY
)
# ★ 显式接住 LOOP/CAP 计数 —— 这正是"信号在手 ≠ 信号被读"那一条:
# 递推里**算出了** LOOP,但第一版脚本没有任何断言读它 ⇒ 造环的变异**存活**。
# (本脚本自己也犯过一次这个错,记在这里。)
SUPP_MAIN="${SUPP%% loops *}"
SUPP_LOOPS="${SUPP##* loops }"
SUPP_FULL="${SUPP_MAIN%% *}"; SUPP_FAIL="${SUPP_MAIN##* }"
S_FULL_G="${SUPP_FULL#full:}"; S_FULL_G="${S_FULL_G%%/*}"
S_FULL_S="${SUPP_FULL##*/}"
S_FAIL_G="${SUPP_FAIL#fail:}"; S_FAIL_G="${S_FAIL_G%%/*}"
S_FAIL_S="${SUPP_FAIL##*/}"
L_FULL="${SUPP_LOOPS%% *}"; L_FULL="${L_FULL#full:}"
L_FAIL="${SUPP_LOOPS##* }"; L_FAIL="${L_FAIL#fail:}"
if [ "$JSON" = "1" ]; then
printf '{"total":%s,"bound":%s,"placeholder":%s,"kinds":"%s","kind_failure":%s,' \
"$TOTAL" "$BOUND" "$PLACE" "$KINDS" "$KFAIL"
printf '"kind_unknown":%s,"class_naive":%s,"class_real":%s,' \
"$KIND_BAD" "$NAIVE" "$REAL"
printf '"substr_failure":%s,"prefix_failure":%s,"homeagent_failure":%s,' \
"$SUBSTR_N" "$PREFIX_N" "$HOMEAGENT"
printf '"residue":%s,"residue_unbound":%s,' "$RESIDUE" "$RESIDUE_UNBOUND"
printf '"perm_with_failure":%s,"summary_with_failure":%s,' "$PERM_FAIL" "$SUMMARY_FAIL"
printf '"perm_without_request":%s,"summary_with_request":%s,' "$PERM_NO_REQ" "$SUM_WITH_REQ"
printf '"loops_full":%s,"loops_fail":%s,' "$L_FULL" "$L_FAIL"
# ★ 键名里必须带 **keying**(pi 2026-09-25 实测:生成器用了 `(ag, prefix(k), r)`,
# 但序列化时把 `ag` 丢了 ⇒ 任何人引用这个数都会以为它是"仅根"口径)。
# 实测两种 keying 全局差 6 封(37/6 vs 43/10,6 组全是跨 agent 合并)⇒ 名字里不写,
# 下一个人**必重犯**。scope 同为必需:本脚本报的是**全局**;session 侧同 keying 是另一个数。
printf '"suppress_full_groups_keying_agent_root_scope_global":%s,"suppress_full":%s,' "$S_FULL_G" "$S_FULL_S"
printf '"suppress_fail_groups_keying_agent_root_scope_global":%s,"suppress_fail":%s}\n' "$S_FAIL_G" "$S_FAIL_S"
exit 0
fi
# ── 断言:**不变量与关系**(不随流量变)───────────────────────────────
fail=0
chk_true() { # chk_true <说明> <1=成立 0=不成立>
if [ "$2" = "1" ]; then
printf ' [ OK ] %s\n' "$1"
else
printf ' [FAIL] %s\n' "$1"; fail=1
fi
}
chk_eq() { [ "$2" = "$3" ] && printf ' [ OK ] %s\n' "$1" || { printf ' [FAIL] %s(实测 %s,期望 %s)\n' "$1" "$2" "$3"; fail=1; }
}
printf '\n 取数: %s(只读)\n' "$DB"
printf ' 时刻: %s\n' "$(date '+%Y-%m-%d %H:%M:%S %Z')"
printf ' ⚠️ 下列读数是**快照**(库是活的);断言只看不变量与关系。\n\n'
printf ' ── 当下读数 ──\n'
printf ' total=%s 已绑定=%s 占位=%s\n' "$TOTAL" "$BOUND" "$PLACE"
printf ' distinct kind = [%s];kind 非 permission/summary = %s;kind="failure" = %s\n' \
"$KINDS" "$KIND_BAD" "$KFAIL"
printf ' 类: kind<>'"'"'failure'"'"'(**空真**)=%s;真判据(permission OR key NOT LIKE %%failure%%) = %s\n' \
"$NAIVE" "$REAL"
printf ' failure 边界: %%failure%% 子串=%s;三前缀(无 homeagent)=%s;homeagent:failure: =%s(差 %s)\n' \
"$SUBSTR_N" "$PREFIX_N" "$HOMEAGENT" "$((SUBSTR_N - PREFIX_N))"
printf ' 测试残留=%s(其中未绑定=%s)\n' "$RESIDUE" "$RESIDUE_UNBOUND"
printf ' 抑制数 [keying=(agent,根) scope=全局]: 完整 parent 链=%s 组/%s 封;只沿 failure 链=%s 组/%s 封\n' \
"$S_FULL_G" "$S_FULL_S" "$S_FAIL_G" "$S_FAIL_S"
printf ' 根计算触到 LOOP/CAP 的次数: 完整链=%s;failure 链=%s\n' "$L_FULL" "$L_FAIL"
printf '\n ── 不变量(断言)──\n'
chk_true "total == 已绑定 + 占位(两分穷尽)" "$([ "$TOTAL" -eq "$((BOUND + PLACE))" ] && echo 1 || echo 0)"
chk_true "kind 只取 permission/summary(写路径构造决定)" "$([ "$KIND_BAD" -eq 0 ] && echo 1 || echo 0)"
chk_eq "kind='failure' 的行数恒为 0" "$KFAIL" "0"
chk_true "**空真**: kind<>'failure' 的计数 == 已绑定(对全表恒真)" \
"$([ "$NAIVE" -eq "$BOUND" ] && echo 1 || echo 0)"
chk_true "真判据 **严格小于** 空真判据(否则判据没起作用)" \
"$([ "$REAL" -lt "$NAIVE" ] && echo 1 || echo 0)"
# ★ 量的是**包含关系**,不是"恰好只有四族":
# 三个前缀字面量本身都含 "failure" ⇒ 它们匹配的键必然也被 `%failure%` 匹配(子集)。
# 而 homeagent 族既不以那三者为前缀、又含 failure ⇒ 必落在**差集**里(子集)。
# 两条都由机制决定。先前写成 `HOMEAGENT == SUBSTR_N - PREFIX_N`,是把
# "当下只存在这四个族"当成了不变量 —— 任一桥改用第五种拼法(如 `pi-failure:`)
# 会让差集 +1 而 homeagent 不变 ⇒ **假红**(新增命名是正常演进,不是故障)。已实测复现。
chk_true "三前缀 ⊆ 子串(三前缀字面量都含 failure)" \
"$([ "$PREFIX_N" -le "$SUBSTR_N" ] && echo 1 || echo 0)"
chk_true "homeagent 族 ⊆ 差集(它非三前缀之一、且含 failure)" \
"$([ "$HOMEAGENT" -le "$((SUBSTR_N - PREFIX_N))" ] && echo 1 || echo 0)"
# ★ 那两行残留是**快照,不是不变量** —— 与文件头 24-27 行同一条规则。
# 未绑定那行是"claim 后早退没退键"留下的化石;**清掉它是正确动作**,
# 而 `RESIDUE == 2 && UNBOUND == 1` 会把那个正确动作判成失败
# (实测:清后 1/0 ⇒ 假红)。⇒ 改为只打印(见上面"当下读数"),
# 另用一条**与清理无关**的关系接住:未绑定必是占位,口径一致。
chk_true "残留中未绑定的 ⊆ 占位行(未绑定必是占位,两者口径一致)" \
"$([ "$RESIDUE_UNBOUND" -le "$PLACE" ] && echo 1 || echo 0)"
chk_true "两口径抑制数**不同**(沿链范围不同 ⇒ 量的是不同集合)" \
"$([ "$S_FULL_S" -ne "$S_FAIL_S" ] && echo 1 || echo 0)"
chk_true "完整链口径的抑制数 **大于** failure 链口径" \
"$([ "$S_FULL_S" -gt "$S_FAIL_S" ] && echo 1 || echo 0)"
# ★ 下面两条是补的"信号被读":递推里算了 LOOP/CAP,第一版没人读它 ⇒ 造环变异存活。
# 断言"没有触到上限"= 断言"根计算是可信的"。库真有环时它会红 —— 那正是我们要知道的。
chk_eq "完整 parent 链的根计算**未触上限**(无环/无超深)" "$L_FULL" "0"
chk_eq "failure 链的根计算**未触上限**" "$L_FAIL" "0"
# ★ 变异③的补丁: 占位行数本身就是"幂等键是否泄漏"的读数据 —— 光有 total=bound+place 不够
# (③ 把一行置 NULL,两边同时变,恒等式仍成立 ⇒ 存活)。
#
# ⚠️ 但**不能断言 `占位行 <= 1`**(pi 2026-09-25 实测,我复跑确认):
# `ClaimRelay`(INSERT 占位)与 `BindRelayMail`(UPDATE 绑定)**不在一个事务里**,
# 所以"同时有 2 个请求在飞"的正常形态就是 2 个占位行(t1 claim、t2 claim、t1 bind、t2 bind)。
# 实测(/tmp/pl/t.db:清掉化石 + 插 2 个合法在飞占位):
# PLACE=2 ⇒ 旧判据 rc=1,报 [FAIL] 占位行 <= 1 —— **把健康并发判成泄漏**
# ⇒ 阈值 1 是"只有 1 个请求在飞的那天"的快照,与它要防的病(泄漏)**不正交**。
#
# ⇒ 与泄漏正交的判别量是**行的年龄**:正常占位行只活"一个请求的时长",泄漏行活到**下一次请求之后**。
# 年龄与并发度无关(并发再大,每行寿命仍是一轮请求),也与"此刻是否恰好有请求"无关。
# 阈值取 10 分钟:远大于任何单个请求(毫秒~秒级,且 HTTP 有超时),
# 又远小于真实泄漏的尺度(那 1 行活了 12.7 天)。
# ★ 断言的是**上界**而不是"是不是 0"—— 因为 0 是"恰好没请求在飞",不是健康的条件。
IDLE_PLACE=$(q "select count(*) from relayed_mails where mail_id is null and (julianday('now') - julianday(created_at)) * 1440 > 10;")
IDLE_AGE=$(q "select cast(max((julianday('now') - julianday(created_at)) * 1440) as int) from relayed_mails where mail_id is null;")
printf ' 占位行年龄: 最老 = %s 分钟;其中 >10 分钟(疑似泄漏)= %s\n' "${IDLE_AGE:-0}" "$IDLE_PLACE"
# ★ 只打印不判红当且仅当**没有任何占位行**:那种情况下 IDLE_AGE 为 NULL/空,
# "没有样本"不等于"样本健康",不能拿它当绿(本文件反复记的假绿形状)。
chk_true "没有**超过 10 分钟**的占位行(年龄与并发度正交;阈值 1 会把 2 个在飞判成泄漏)" \
"$([ "$IDLE_PLACE" -eq 0 ] && echo 1 || echo 0)"
# ★ 变异⑤的补丁: 上面那些都是**关系**断言(A=B、A<B),而把一行 permission 的键改成
# failure 形状时,substr 与 prefix **同增** ⇒ 所有关系仍成立 ⇒ ⑤ 存活。
# 关系断言测不出"**语义**被搬动",只有**锚定到写路径**的不变量能测。
chk_eq "permission 行不含 failure 字样(写路径: 键=session:tool_use_id)" "$PERM_FAIL" "0"
# ★ 跨表 provenance 断言(接住 ⑤b:改了 kind 但计数关系全不变)
chk_eq "kind=permission 的邮件**都**有 permission_requests 行" "$PERM_NO_REQ" "0"
chk_eq "kind=summary 的邮件**都不**在 permission_requests 里" "$SUM_WITH_REQ" "0"
printf '\n'
if [ "$fail" = "0" ]; then
printf ' relayed_mails 口径复算:全部**不变量**成立\n'
exit 0
fi
printf ' relayed_mails 口径复算:**有不变量不成立** —— 被数的东西变了,或库换了\n'
exit 1