Files
MailUI4Agents/deploy/check-relay-counts.sh
JianFeeeee 60d59f9de0 新增 deploy/check-relay-counts.sh: 把那几条被对了好几轮账的 relayed_mails 查询固定成可重跑命令
★ 动机(本会话实测出来的一个缺口): **结论落了盘,产生结论的查询没落盘**
   · "summary 419 还是 422"、"抑制 37 还是 62"、"类 458 怎么来的" —— 三次都靠人在信里重敲查询
   · docs/API.md 留着那些数,却**没有一条能整段重跑的命令** ⇒ 每个复算的人都要从头重写 ⇒ 必然再分叉
   ⇒ 本脚本把查询固定成命令;谁不同意某个数,跑它、看输出

★ 断言的是**不变量与关系**,不是冻结的数(库是活的,冻数明天必假红)
   · 不变量/关系 ⇒ 断言;当下读数 ⇒ 只打印(附时刻)
   · 16 条断言,基线 rc=0

★ 变异测试: 9 条变异**全部被接住**(基线绿 ⇒ 变异 rc=1)
   ① kind=failure ② 删占位 ③ 置 NULL ④ homeagent 改名 ⑤ perm 键改 failure 形状
   ⑤b perm→summary+failure ⑥ 删已绑定残留 ⑦ 造环 ⑧ 删一条 permission_requests

★ 三条断言是**被变异逼出来的**,值得记(都是"我自己的脚本先犯了那个错"):
   · ⑦ 造环最初**存活** —— 因为递推里算出了 'LOOP' 却**没有任何断言读它**
     ⇒ 正是我同一轮报给 pi 的"**信号在手 ≠ 信号被读**"(我的脚本也犯了)
   · ③ 最初存活 —— total=bound+place 是恒等式,两侧同变仍成立
     ⇒ 真正该断言的是**占位行 <= 1**(>1 即"占位后没退键"的泄漏信号)
   · ⑤b 最初存活 —— 改 kind 时所有**关系**断言仍成立
     ⇒ 只有**跨表 provenance**能抓: permission 行的邮件必有 permission_requests 行、summary 行的必无
     ⇒ 记法: **关系断言测不出"语义被搬动",只有锚定到写路径/别的表的不变量能测**

★ 口径声明写在文件头(本脚本存在的主要理由): 已绑定/占位两分、类判据是 relay_key 形状而非 kind、
   抑制数给**两种口径**(完整 parent 链 61/37 vs 只沿 failure 链 92/6 —— 差很多)

★ 顺带修掉一个我自己引入的权限违规: 34a15dc 的 permission_relay_release_test.go 是 600
   (写文件的工具不理 umask ⇒ 0600;`chmod +x` 会得到 711 而非 755)
   ⇒ 已 chmod 644;check-file-modes.sh 现在只剩 1 个既有违规(非我的 scope 文件)

★ 边界: 只读打开库(mode=ro);生产一个字节没动;/tmp 变异副本已用后即弃
2026-09-25 06:08:59 +08:00

234 lines
14 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/check-relay-counts.sh [--json]
# 退出码: 0 = 全部**不变量**成立;1 = 有不变量不成立
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"
printf '"suppress_full_groups":%s,"suppress_full":%s,' "$S_FULL_G" "$S_FULL_S"
printf '"suppress_fail_groups":%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 ' 抑制数: 完整 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)"
chk_true "子串 %failure% **多于** 三前缀(差即 homeagent 族 = 命名巧合)" \
"$([ "$SUBSTR_N" -gt "$PREFIX_N" ] && echo 1 || echo 0)"
chk_eq "homeagent 族 == 子串 − 三前缀" "$HOMEAGENT" "$((SUBSTR_N - PREFIX_N))"
chk_true "残留集合 = 1 未绑定 + 1 已绑定(**不是同一行**)" \
"$([ "$RESIDUE" -eq 2 ] && [ "$RESIDUE_UNBOUND" -eq 1 ] && 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**:
# 正常路径下 Claim 后立刻 Bind/Release,占位是**瞬时**的;稳定 >1 意味着有键没退。
chk_true "占位行 <= 1(>1 即"占位后没退键"的泄漏信号)" \
"$([ "$PLACE" -le 1 ] && 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