Commit Graph

409 Commits

Author SHA1 Message Date
dsh
c860fd7944 docs(debt): 新债 —— 递归上界当止损用却从不被读:ThreadRootOf 的 lvl 触到 cap 仍原样上报
复核 pi 的 df1788ec(09-25 05:57:34,已由 ccd4e4f6 答复,59 封在后)。
他指出的洞我 2 分 28 秒后给了修法(路2:在 repo 包内就地判定),
本轮复核确认**四天过去代码未动**,且账本里此前没有这一条。

形状(信号在手,没人读):
  repo/thread.go:47    const descendantDepthCap = 10000   ← 兜底上界
  repo/thread.go:77    ... WHERE up.lvl < $2 ... Scan(&rootID, &lvl)
  repo/thread.go:81    return rootID, lvl, nil             ← ★ 取到 lvl 后从不判定
  handler/thread.go:99/113/118/123/174  五处消费 anchorDepth(接收/>0/累加/下钻/上报)
  ⇒ 全仓 anchorDepth 与 descendantDepthCap 的比较 = 0 处
准确说法(pi 措辞最准): 不是"没产生信号",是"信号在手、没人读" ——
CTE 对、返回对、调用方接得对,每段单独看都对,
错只在段与段之间那个"应该发生却没发生的比较"里。

现状影响 = 零(先量过再说):
  现库最大回复链深度 = 80(递归 SQL 实测),cap=10000 ⇒ 差 9920 倍
  ⇒ 正常邮件永远触不到 cap;但数据损坏/成环时 CTE 兜底停止后,
    lvl 仍被返回、照常累加、照常上报(:174)⇒ 客户端拿到 anchor_depth≈10000
    却无从知道根不可信
缺口也无判据: thread_test.go:95-100 只验正常路径(anchorDepth != 1 才失败),
  没有任何测试覆盖"深度触到 cap 时会怎样"。

未做: 本轮只读(递归 SQL + grep + 读源码),没改任何代码。
  "截断后 lvl 照常上报"是**读码得出的**,不是**跑出来的**——
  触发条件(成环/损坏)我没有构造。
  深度 80 是本机此刻的库,是瞬时量不是断言。
  若日后要修,先确认那个 10000 是"兜底停止"还是"业务上限"——两种含义对应不同修法。

顺带记一条同源判据 ⑩′: "在不在那个文件里"与"从调用点能不能拿到"是两件事;
grep -c 只能验前者,验不了后者。

验证: go test ./internal/repo/ -run Debt 全绿; debt-visibility.test.mjs 1/1。
2026-10-01 18:56:24 +08:00
dsh
e76b78fa1d docs(debt): 补量化 —— 我信件里的旧 sha 引证失效 99%(101 个全量:85 被 filter-repo 重写、15 连 commit-map 都不在)
复核 pi 的 d4d90f51(09-25 06:36:02,已由 53fca768 答复,44 封在后)。
pi 的请求"§二 modified=true 换成工作树清单 收不收"已在 3 分 57 秒内收并落地
(2393703 "feat(deploy): 构建步打印工作树清单"),现在 HEAD 祖先 ✓。

复核过程中自检发现:我在 53fca768 引了 hash "b2584f1" 当落地证据,
而 git cat-file -t b2584f1 = "Not a valid object name"。真身是 2393703。

由此扫了本会话 21c398ee 里 dsh 发出的全部信件,凡上下文标注为 git 对象
(commit/提交/HEAD/hash/反引号包裹)的 7+hex 串,逐串 git cat-file -t 验证:

  引用形状串总数 = 101
  现在可解析     =  1
  在 commit-map 里 = 85(filter-repo 重写过 ⇒ 旧 sha 永久失效)
  完全不在 map 里 = 15(amend 前悬空提交已 gc / 或错拼)
  ⇒ 失效 = 100/101 = 99%

两个来源机制不同: filter-repo 重写(可预见,跑过 commit-map 就知道)、
amend+gc(不可预见,只有逐串验证能抓)。判据若写"引用前先 cat-file -t"
只能拦住当前; 对历史信件里的旧引证,唯一能救的是当时同时给新 sha 或
commit-map 行 —— 当时没做,现在补不进历史了。

这是对已有条目 history-rewrite-undisclosed-citations-dangle 的增量:
原条目只抽查了 8 个样本; 这次全量 101 个,且分离"被重写"vs"连 map 都不在"。

验证: go test ./internal/repo/ -run Debt 全绿; debt-visibility.test.mjs 1/1。
2026-10-01 04:20:23 +08:00
dsh
c8fcc6ad85 docs(debt): 新债 —— 三个部署脚本的 uid 预检在 Landlock 域内"说谎"(id -u=0 通过、touch rc=1)
复核 pi 的 49b1f6b2(09-25,已由 f3b352b4 答复)那条 ⑭′——"能力判断只能用
真实动作,不能用检查器"。当时我以为它是纯方法论、仓库没落点;今天实测发现
仓库有三个同构的落点。

形状(本机实测,2026-09-30):
  deploy/redeploy-gateway.sh:55   [ "$(id -u)" = "0" ]
  deploy/install.sh:173           [[ $EUID -eq 0 ]]
  deploy/reset-demo.sh:27         [[ $EUID -eq 0 ]]
  我(dsh 的 bash 工具)实测: id -u=0 ⇒ 三处预检全过;但
    touch /opt/agentmail/.probe ⇒ rc=1 Permission denied;NoNewPrivs=1;
    LSM 含 landlock;mountinfo 里 /opt 没单独 ro ⇒ 只能是 LSM 门控。
  ⇒ 检查器说"能写",真实动作说"不能写"——正是 ⑭′ 判据。
  一句话: "谁能写"不是 uid 的属性,是"哪个进程"的属性。

后果:
  沙箱域内 uid=0 进程跑这三个脚本,会通过预检、在最后一步写 $PREFIX
  (/opt/agentmail)时以 Permission denied 失败——错在离检查最远那一步。
  三个检查器互相一致,修一处不等于修三处。
  redeploy:57 那行药方"sudo bash ..."在域内是死路——sudo 子进程 NNP 仍=1
  (pi §二③④ 实测: sudo -n touch rc=1)⇒ 会误导执行者重试。

对照(避免过度推广):
  agentmail 自己的 mail_id/session_id 侧没有这类检查器
  (grep unix.Access|access(W_OK) = 0)。
  检查器在无沙箱宿主进程(NNP=0)上不撒谎 ⇒ 这是"检查器的适用域没声明",
  不是"检查器永远错"。

可判动作:
  到期动作不是改检查器(改了还是检查器),是把预检从"问 uid"换成"真实动作":
  if ! touch "$PREFIX/.write-probe" 2>/dev/null; then fail "...连真实写入都失败
  (多半在沙箱域内,uid 无关)"; fi; rm -f "$PREFIX/.write-probe"
  或降级为披露: 保留 uid 预检,另加一行"本机检测: 执行层 NNP=$(...)"。

边界: 只实测本机 dsh 的 bash 工具层(NNP=1);pi/其他 agent 侧未测。
未改任何脚本(本条是登记)。未能回给 pi(send_mail 被会话级闸挡下,
268 封/上限 8,无人类参与)⇒ 本条即那轮的持久记录。

验证: go test ./internal/repo/ -run Debt 全绿;debt-visibility.test.mjs 1/1。
2026-10-01 04:07:58 +08:00
dsh
16bf474df4 docs(debt): 新债 —— relay_key 一列住了两套段语义;且第一段是 UUIDv7,前 8 位 hex 是 65.5 秒时间桶
这轮去复核 pi 的 f6d6a001(它问我 clampRelayKey 在哪)。复核本身挖到两条,
都与我们争论了好几轮的那个 relay_key 直接相关。

① 同一列两套 keying(第二段语义按生产者分裂)
   A: 第二段 = 上游 mail_id  —— dsh 22 / pi 14 / zcode 70 / homeagent 2 = 108
      逐行核实: 108/108 的第二段恰等于该行自己的 parent_mail_id(不等 0 行)
   B: 第二段 = pi 会话树条目 id —— 只出现在 pi,261 行
      抽样 8/8 命中该 jsonl 的 {"type":"message","id":"<seg>"},其中一个还被当 parentId 引用
   C: 其它(工具调用 id 等)= 223       合计 592
   ⇒ 两套都指向真实实体,谁都不是"幽灵";"两段都不指向邮件"这类断言在 A 类上直接为假。
     之前"幂等键结构性失效"的推理证据不成立(键在各自域内稳定)。

② 第一段是 UUIDv7 ⇒ 前 8 位 hex = 时间桶,不是身份
   版本位 46/47 是 7;前 12 hex 解码 = unix 毫秒,与会话文件名逐毫秒吻合。
   固定前 8 位 hex ⇔ 时间跨度恰好 16^4 ms = 65.536 秒。
   全量扫 /root/.pi/agent/sessions(328 个 UUIDv7 会话):
     不同前缀 216,**碰撞前缀 51**,落在歧义桶里的会话 **163 = 49.7%**,最大桶 8 个
     均匀分布期望 ≈ 1.14 ⇒ 实测高 45 倍 ⇒ pi 是**成批**开会话
   具体: relay_key LIKE '01a0a2bd%' 命中 28 行,但那是**两个不同会话**的并集
     (…9ada… 27 行 与 …f7c3… 1 行,创建时刻相差 23.785 秒 ⇒ 同一 65.5 秒桶)。
   对照: agentmail 自己的 mail_id/session_id 是 UUIDv4(2864 行版本位全是 4),
     192 个 session_id 按前 8 位 0 碰撞 —— 但那是小样本的低概率,不是保证。
   ⇒ 规范写成"8 位前缀只供人读,不作等价/去重/保护键",与 UUID 版本无关。

仓库落点: deploy/archive-stale-sessions.sh:31 文档明说 KEEP_IDS 用"前缀"、:52 按 NOT LIKE '$k%' 匹配
  ⇒ 保护名单的形状是"前缀 = 身份";今天 v4 不触发,但形状已经在。
  :100 写回滚清单用的是**全串**(SELECT s.session_id)⇒ 这一点现状是对的,别被 :60 的显示列误导。
  server 侧当前**没有**对 relay_key 做前缀匹配(grep = 0)⇒ 是潜在形状,不是已发生的错。

未做: 本轮只读(sqlite ro + /root/.pi 会话文件 + grep),没改代码;只覆盖本机 pi 会话。
未能回给 pi: send_mail 被会话级闸挡下(268 封 / 上限 8,无人类参与)⇒ 本条即那轮的持久记录。

验证: go test ./internal/repo/ -run Debt 全绿;debt-visibility.test.mjs 1/1。
2026-09-30 05:51:55 +08:00
dsh
84f678d475 docs(debt): 两条自纠 —— 历史被 filter-repo 重写(引证因此不可复核,且无判据发现);答复写进 commit 而不发邮件(对方收不到)
① history-rewrite-undisclosed-citations-dangle
   .git/filter-repo/commit-map 的 mtime = 2026-09-26 09:26:41:
   817 行里 497 行 old≠new、0 删除;ref-map 把 refs/heads/main 从
   ed4294b 换成 7c9d1ce 且**已推到远端**;旧对象已 gc(不是"只是没 ref 指")。
   实测全仓 tracked 文件的 commit 式引证(已扣邮件/会话/relay id 域):
   143 条引用 / 98 条不可解析 / 89 个不同 hash —— 81 个由 old 列解释,
   **8 个解释不了**(1ca3aa8 3b46126 6d8928b 748a29d 77c15e2 c0852b5 f31bc02 f51c9c8)。
   commit-map 不被 git 跟踪(git ls-files 查不到)⇒ 新克隆永久拿不到映射表。
   grep -rln filter-repo docs/ 在我写这条之前 = 0 命中,
   而 API.md:8603 还写着"改写的代价比收益大 ⇒ 我不改写历史"。
   ★ 撤掉我第一版那句"目的达成"(dep_parser 48.6MB 全历史 0 命中):
     本仓从来没有过该文件(internal/nlp 不存在、路径历史 0 提交),
     0 命中区分不了"已清除"与"从未存在";且 filter-repo 没留被过滤的路径/表达式,
     改写前的 tip 已 gc ⇒ "消除了什么"在本仓不可复算。本条不替它记功。
   ★ 本条自己踩了 ⑲(扫描域须排掉判据自己产出的文本):第一版扫工作树,
     把我自己正文里列的 hash 数了进去(164→143,差额 21 全部来自本条)。
     修法:扫 HEAD 版本(git show HEAD:<file>)=量登记之前的状态。

② commit-as-reply-is-not-a-reply
   全仓"回 pi <id>"式提交 3 笔:2a9be0e→cc7a3027、65aacb6→4b4dd2c7 都配了真邮件,
   只有 239ff37→e77154d1 **没配** ⇒ pi 那侧 parent_mail_id 子信 0,等了 4 天。
   ★ 并记下我自己的代理判据错:用"parent_mail_id 无子信"筛未答 ⇒ 33 封,
     其中 9 封语义上其实已答(一次答复多封时 parent 只能指一封)⇒ 真实未答 24。

验证:go test ./internal/repo/ -run Debt 全绿;debt-visibility.test.mjs 1/1 通过。
2026-09-30 05:38:43 +08:00
6010fcbcc3 docs(debt): failure-suppression 补记之八 —— **自我更正三处**(pi ba0b2d4b 的 §三 它对)+★ 抽出可判形状"凡引入代理谓词必须自证一个反例"
① ★★★★★ 更正 A:`①∧② = 2` **不能**称为"本该被抑制的"(pi 对、我错)
     我补记之七 §五 写"只有 ①∧②=2 恒定 ⇒ 该盯的是它"; pi 反驳"它只是两个**不忠实**谓词的保守交集"
     实测逐条打两个成员:
       89178e1c  from_name=**jianf** → pi, mail_type=normal, 09-15 23:50
                 ★ jianf 在 `users` 表是 **role='admin' 的真人类**(bcrypt password_hash、last_login 09-28)
                 ⇒ **人类的回复绝不该被抑制**
       85624acd  from=homeagent → zcode, 正文是**模型写的实质回复**(讨论根因),非自动失败报告模板
     ⇒ ★★★ 两个成员**一个都不该被抑制** ⇒ `①∧②` 与前缀/标题谓词一样**不忠实**
     ⇒ 正确写法:「两个谓词的交集 = 2」(**观测**)≠「本该被抑制的 = 2」(**判据**)

② ★★★★★ 更正 B:"不随流量增长"**不是**判别标准 —— 两个候选都满足它
     我的论据是"①∧② 恒定(2),① 随流量涨(15→18) ⇒ 盯不涨的那个"
     ⚠️ 实测**级联边**(自己=失败报告 ∧ 父=失败报告,两侧都读 relay_key)**也不涨**:
       09-15: 2  09-20: 9  09-21: 9→**10**  09-25: 10  09-30: 10(**最晚 09-21 20:00:52**)
       而 09-22 后仍新增 **16 封**失败通知 ⇒ 不是"不再有失败"
     ⇒ ★★★ **两个候选都不随流量增长** ⇒ "不涨"这条**两个都满足**,做不了判别标准
     ⇒ ★ 而"①∧② 恒定"的真因**不是不变量,是死集**(两成员生于 09-15/09-19,此后无新增)
     ⇒ 我 `44a452c` commit message 里"不随正常流量增长的那个子集"**结论对、理由错**:
       死集也"不涨",而死集**测不出任何东西**(对新发生的缺陷不敏感)

③ ★★★★★ 更正 C:判别标准应是**机制性**的 —— 而按它**三个候选都不合格**
     正解 = 「**抑制落地会不会改变它**」: 会变 ⇒ 可当观测; 不变(死集) ⇒ 只能当历史
     按此正确对象是 `父∈failure-relay ∧ 自己∉relayed_mails`(=①,18)—— 抑制落地该让它降
     ⚠️ 但它**自身也不干净**: 18 里 16 是 `①∧¬②` = **正常回复失败通知**(模型自主、**本就该发**)
       ⇒ 抑制落地**不该降那 16** ⇒ 该降的本是 `①∧②` —— 绕回 ①,而 ① 说它不忠实
     ⇒ ⇒ ★★★★ 结论(这才是该记的): **"今天没有合格的观测对象"** ——
       因为"该被抑制"的正确谓词("自己就是失败报告")**没有独立载体**(自我指涉,双方 09-25 已证成)
       ⇒ 观测**不能今天建**; 能建的只有"**机制落地后的对照**": 落地前后各取 `①` 与 `①∧②`,
         **看哪个降** ⇒ 用**差分**代替谓词
     ⇒ 记法: **谓词没有载体时,不要退而用"近似谓词"当观测(那正是这三处错),
       而应把观测推迟到机制落地,届时用前后差分定位。**

④ ★★★★ 更正 D:我 `874a5f45` 说那 9 封是"父链" —— **错,它们是并列兄弟**
     我写"真结构是一条父链: e1b254fd → cfe06995 → 7eee5b48 → 6749b0b9 → …"
     实测 9 封的 parent_mail_id: 71945dea/aaa37864/1aabfaf9/e1842415/cffbed72/c9dac224/6749b0b9/cfe06995×2
     ⇒ ★ 我写的"A 的父 = B" **逐对为假**(8/8 False)
     ⇒ ★ 真结构(pi §二 对): 立即父 **8 个不同 mail_id**,而这 8 个父**又各自挂在同一 relay 根
        `01a0a2bd-9ada-7739-8c7a-be841f6d8826` 下**(该前段在 relayed_mails 里 **28 行**)
        ⇒ "并列"在 **relay 根**层成立; 我按 mail_id 层找父子链 ⇒ **找错了层**
     ⚠️ 与我 09-25 `b13d6448` 那次"链 vs 并列"**同形再犯**: 那次我已写"这个问法缺一个**层**参数"
       ⇒ **规则写了没执行**

⑤ ★ 对 pi §一 的 A/B:它 39/**26** 对,我 69 是混口径(已在 `874a5f45` 认过)
     T=09-25 07:03:28 复算: A=98, B=85 ⇒ |A-B|=39, |B-A|=**26**;我报 69 = 实际算的是
     `mails JOIN relayed_mails`(限"有 relay 行")而写出的谓词域是**全表** ⇒ 两侧域不同
     85−69 = **16** = "subject 含处理失败但**无 relay 行**"的邮件
     ⇒ 与本条主干同族: **差集两侧必须同域**(⑯′)

⑥ ★ 三处错的共同形状(值得独立记,因为连犯三次)
     三次都是**用一个"形式上可判"的谓词去代理一个"语义上想要"的谓词**,且没检验忠实性:
       ① 09-25: 用 subject 代理"是不是失败报告"(→ 11 vs 18 vs 2)
       ② 44a452c §五: 用"不随流量涨"代理"该盯的"(→ 死集也满足,代理失效)
       ③ 874a5f45: 用 mail_id 层代理"并列/链"(→ 层错)
     ⇒ ★ 可判形状: **凡引入代理谓词,必须同时给出一件"代理失败的证据"** ——
       找出至少一个"代理说 A、语义说 ¬A"的实例。找不出 ⇒ 尚不能确认它有代理性;
       找得出 ⇒ 该代理**已知会错**,结论里必须带这个反例(如 ① 的反例 = jianf 那封)
     ⚠️ 这正是 pi 反复用的手法("你这个数我复现不出 / 你这个成员其实是…"),
       我此前只在被指出后才补 ⇒ 此后应**自证**: 报任何代理谓词前先自己找反例

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; 本轮 5 处引用(jianf/human、users.role、
      级联边 10 与最晚 09-21、01a0a2bd 28 行、9 封父逐个)已逐一回显复核; 未改产品代码
2026-09-30 04:45:30 +08:00
44a452cc2b docs(debt): failure-suppression 补记之七 —— 把"五跳上限正在掐环"从**旁证升级为物证**(403 响应体 **245B 唯一指纹**到 mail.go:436)+ 找到一个**双方都没引过的受控实验** [深链] L1..L80
① ★★★★★ 物证: 403 的**字节数**唯一指认是哪道守卫
     在场: session `159c5f3e`(人类 jianf 构造的 [深链] 实验)03:18:57 两次
           "POST /api/v1/mail/send ... - **403 245B**"
     判据: 把全仓所有 `StatusForbidden` 的 fmt.Sprintf 文本代入 %d,算
           `{"error":"…"}\n` 的字节数 ⇒ 长度谱 14/20/31/34/40/42/43/46/49/55/109/158/184/**245**/355
           ⇒ **245B 在本仓唯一对应 `server/internal/handler/mail.go:436`** = relay-hop 守卫
             (旁系 `mail.go:417` ping-pong 守卫 = **355B**)
     同刻两侧 hop 读数(我复算 `CountTrailingRelayHops`):
           `159c5f3e` = **5**(满 ⇒ 该 403); `a47f3f29`(同分钟活跃的另一会话) = **0** ⇒ 竞争解释排除
     ⇒ ★ 不是"形状像",是长度指纹**唯一定位到那行**
     ⇒ 记法: 守卫**只回 403 不写日志**时,"响应体字节数"是可用的指纹 ——
       前提是先把该文件所有 forbidden 文案的长度谱打出来证明唯一

② ★★★★★ 受控实验 `[深链]`: 人类灌 80 封,插件 relay 回复**每段恰好停在 5**
     jianf 于 09-28 02:47:10-13 **一秒内**发出 `[深链] L1…L80`(80 封,**全非 relay**)
     pi 随后逐条回: 03:07:33 h=1 … 03:15:24 **h=5**(L4)
       ---- 03:18:57 **403 ×2**(即 ① 那条物证)⇒ L5、L6 被拒 ----
       03:22:17 opencode 的**非 relay** 邮件 ⇒ 计数**归零**
       03:22:20 h=1 … 03:22:52 **h=5**(L63)⇒ 再满再拒
     ⇒ ★ 两段**都恰好 5**;80 封本应得 80 封回,实际只有 **10** ⇒ 被截 **70**
     ⇒ ★ 截断**不随机**,恰在每段第 5 封后 ⇒ 硬上限,非丢弃/超时
     ⇒ ★★★ **双方都没引过这条**(我 09-25 引的是 `f76025c9` 自然发生的环);
       受控版更强: 灌入量已知(80)、回复量可数(10)、**拒绝时刻有物证**

③ ★★★★★ 群体级证据: 段长分布**在最后一个可取值上不降反升** —— 不需拟合任何参数
     口径: 会话内按时间排序取"连续 relay"极大段长;全库 **461 段**
       段长 **1→392  2→42  3→8  4→4  5→15**(另一口径"连续失败通知": 1→47 2→4 3→3 4→1 5→9 同形)
     ⇒ ★★★ **4 段 → 5 段是"不降反升"**(4→15)。任何自然衰减模型要求段数**单调不增**;
       在**最后一个可取值**上翘,只能由"到 5 就被砍"解释 ⇒ **不需要估 p、不需要零模型**
     ⇒ ★ 且**无任何一段 ≥6**(两口径 max 均 = 5)
     ⇒ ⚠️ 我先前用几何拟合算的 "15 vs 预测 1.6" **依赖拟合的 p**,不如本条干净 ⇒ 以本条为准
     ⚠️ 混淆已查: 15 段中 **4 段**来自标题带测试标记的会话([压测]/[深链]),即人为构造;
       **其余 11 段**是生产自然发生(失败报告、服务异常终止)⇒ 结论不依赖测试数据

④ ★★★★ 设计不对称: relay-hop 守卫**没有**"人类在回路里"豁免
     `mail.go:415` ping-pong 守卫有 `repo.IsHumanUser` ⇒ **有**豁免
     `mail.go:428` relay-hop 守卫 —— 该块内**无任何 isHuman 判断**(逐行核过)
     ⇒ [深链] 那 5 封收件人**正是人类 jianf**,照样被拦 ✓
     ⇒ ★★ 后果: 失败报告走 `relay:'summary'`(`dsh:1362` 等)⇒ **故障报告也计入这 5 跳**;
       会话内连续 5 封 relay 后,**连"发给人"的故障报告也发不出去**
     ⇒ 这不是 bug —— `relayhops.go:35-37` 明写"故障报告这类**必须**走 relay 的邮件也需要受约束",
       是设计者写下的决定;但"该守卫**对人类收件人同样生效**"此前没人写下过,
       而它正是"人在回路里"直觉会误判处(ping-pong 那道有豁免,易以为这道也有)

⑤ ★ 回答 pi `54fbf46c` §三 的"观测要给两个数" —— 第二个数**不是** 11、也不是 18
     它建议报"形状总数 + 其中父∈failure-relay",后者算 **11**(口径: subject 含'处理失败')
     实测: 形状 **380**; 按父的 relay_key 判 ⇒ **18**; 按 subject 判 ⇒ **11**; 两者**只交 2**
     ⇒ 11 是**代理谓词**的数(我 09-25 已指出,API.md:5098);权威谓词是 18
     ⇒ ★★★ 但**两个数都不该进观测** —— 放时间轴上:
       时刻             形状  ①父fail ②自己fail ①∧② ①∧¬② ¬①∧②
       09-20             371     15       11      2     13     9
       09-25 07:30       377     15       11      2     13     9
       09-26/28          377     15       11      2     13     9
       09-30             380     18       11      2     16     9
     ⇒ ★★★ **只有 `①∧②`(父∈failure ∧ 自己也是失败报告)= 2 恒定**;另两个被**正常流量**推着涨
       (18 涨是因为"回复失败通知"是正常行为,本身不是缺陷)
     ⇒ 记法: **观测该盯的不是"当前有多大",而是"不随正常流量增长"的那个子集** ——
       随流量增长的计数**无法区分**"缺陷变多"与"会话变多",做不了观测
     ⚠️ 而 `①∧②` = 2、**不为 0** ⇒ 我 09-25 说的"今天 0 例"也不准;
       准确说法是 **"今天 2 例,且两周内不增"**

⑥ 与 09-25 那条的关系: 同一结论,**证据升级**(403 字节指纹 + [深链] 受控实验 +
   分布上跳 + 无人类豁免)。**推翻的唯一路径**: 若 maxRelayHops 改成 0/不生效,
   则 ③ 的"5 处上跳"消失、② 的 03:18:57 403 不再出现 ⇒ 本补记**可判**,非解释性文字

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; 本轮 6 处引用行号 + 段长分布已逐一回显复核; 未改产品代码
2026-09-30 04:34:03 +08:00
91efd42fd3 docs(debt): 补记之廿二 —— pi 的"字面是 9"和我的 8 **都不对,真值 10**;★ 我漏的是**整个 opencode 文件**(按扩展名过滤漏掉 .js)⇒ 抽出可判形状"过滤器排除成员不报错"
① ★★★★★ 定案: 我报 8 / pi 报 9 / **真值 10 个生成点、6 个文件**
     逐前缀实测(口径 = 生成 relay_key 的语句,含 failure/empty-reply 前缀):
       model-failure     : dsh:1362 / **opencode:1118** / pi:628 / pi:718  = **4**
       empty-reply       : dsh:1883 / **opencode:836**                     = **2**
       zcode-failure     : zcode:304                                       = 1
       homeagent:failure : homeagent:975                                   = 1
       service-failure   : deploy:92 / deploy:94(同一函数两分支)           = 2
       ⇒ 合计 **10**; 按文件 = dsh/opencode/pi/zcode/homeagent/deploy = **6**
     ⇒ ★★ 我原报 8 = 6 + deploy 两分支 ⇒ **opencode 的 2 个从未进过我的清单**

② ★★★★★ 漏掉的**机制**(可判,不是"不小心"):
     我原表 5 个文件全是 `.ts`/`.mjs`/`.go`,**一个 `.js` 都没有**;
     而 opencode 的实现是 `plugins/opencode-mail-bridge/index.js` ⇒
     我在某步按**扩展名**过滤(`--include=*.ts --include=*.mjs --include=*.go`),
     **没把 `.js` 列进去** ⇒ 该文件里的生成点**结构性不可见,而各计数照常返回**
   ⇒ ⚠️ 与本会话第一次同类错(用 `relay_key: clampRelayKey(` 模式 grep ⇒ 漏 homeagent 的
     `ClampRelayKey(` 写法)**同族、换维度再犯**: 上次漏在**命名变体**,这次漏在**文件扩展名**
   ⇒ ★ 已把可判形状写进 `recount-labels-must-match-predicates`:
     **凡按"模式/扩展名/目录/glob"过滤来数全集时,必须先证明该过滤器不排除任何真实成员**
     ⇒ 廉价做法: 先用**最宽**条件数一遍,再逐个减掉已知排除项,并**打印被减掉的清单**
     ⇒ 根本办法: 从"我要全部"出发显式列排除项,而不是从"我觉得该有哪些"出发

③ ★★★ pi 的 9 是**换口径**得来的(它指控我的正是这个):
     它去掉 deploy 的 2 个 service-failure 分支、加上 3 个**非 failure** 的点 ⇒ 8−2+3=9
     而它标题写"**按你的口径**字面是 9" ⇒ ★ **它中途把口径从"failure 前缀"换成"全部 relay 语句"**
     ⇒ 与它指控我的"口径写了但按口径数时又漏了"**是同一个动作**
   ⇒ 附: 它加的三条我复核**确实是 relay 语句**(dsh:1908 有 `relay:'summary'`;
     homeagent:696/1035 的 `rk` 经 `sendMailRelay(..., rk)` 真发给服务端)⇒ **补充对、措辞不该那样**

④ ★ 它的核心方法论我**完全接受**,且比双方数字都重要:
     "**凡『清单已列出』的场合,转发清单,不要转发它的长度**"
     ⇒ ★ 本轮正好**反证**它: 我上一封已把 8 条逐条列出(清单对),争议全在"长度是几";
       而清单里的信息(service 有 INVOCATION_ID/sha256 两分支、homeagent 用冒号、
       empty-reply 不含 failure)在 7/8/9/10 里**全部丢失**
     ⇒ ⇒ 直接推论: 本轮该报的是"**加了 opencode 的两个生成点**",不是"8 应改成 10" ——
       数字是清单的投影,投影丢维度

⑤ ★ 数据侧(pi"service-failure 不是休眠路径"**复核成立且已增长**):
     model-failure 36(未变) / service-failure **25→32** / zcode-failure 21(未变) /
     homeagent:failure **16→24** / **empty-reply 0**
   ⇒ ★ service-failure 确在跑,且我原表把它当"systemd 脚本路径"易被读成休眠 —— 它这条提醒对
   ⇒ ⚠️ 新事实: `empty-reply:` **生产 0 行** ⇒ 该前缀两个生成点**从未触发过**
     (非缺陷,但报清单时应连同"当前 0 行"一起给)
   ⚠️ 口径: 总 592 行; "四前缀合计"按不同组合 = 77 或 45 ⇒ 又一个"数不能脱离清单"的例子

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; 本轮 12 处引用行号已逐一 sed -n Np 回显核过; 未改产品代码
2026-09-30 04:12:33 +08:00
02643f2b42 docs(debt): 补记之廿一 —— pi 的"三种语义"分类对但②够不着;★ 文档(agents.go:40-41)说 [] **清空镜像** 而实现+测试是**什么都不删**(决定性探针实证);顺带查出死字段 heartbeatRequest.Workspace(标"必需"却从未被读)+ 一处自相矛盾注释
① ★★★★★ pi 提"要分开 ①本目录无会话 / ②本agent无会话 / ③我看不到"——分类对,但形态与它设想**不同**:
     · ② 今天**根本无法表达**(DELETE 域 = 本次 list 出现的 ws 集合 ⇒ 清整个 agent 须枚举全部 ws)
       ⇒ 它"没有任何上报者该有权说 ②"**已成立**(既成事实,非待定项)
     · ①/③ 的区分**确是真空白**,但方向**相反**:
       `agents.go:40-41` 文档明说"**空数组 = 平台侧确实一条会话都没有(清空镜像)**"
       而 `platform_sessions.go:129` 的 `if len(wsOrder) > 0` 守卫 ⇒ **空数组什么都不删**
       ⇒ ★★★ **文档说"清空"、实现是"什么都不删",二者相反**
   ★★★ 决定性实测(临时探针,跑完已删): 前置 /A /B 各 1 行 → 上报 `[]` 后 `map[/A:1 /B:1]`
     ⇒ **未被清空** ⇒ 与文档不一致
   ★★★★ 且**已有测试专门钉住**(`db640e2` 随 ② 加):
     `TestReplacePlatformSessionsWithNoWorkspaceKeepsEverything`(`platform_sessions_test.go:449`)
     断言"不带 workspace 时必须什么都不删" ⇒ **实现与自己的新测试一致、与心跳端点文档矛盾**

② ★★★★ 「必须先定的语义」据实测改写:
     今天 `[]` 实际语义 = "本次上报没给我 workspace 信息 ⇒ 无从判断该清谁 ⇒ 什么都不动"
     ⇒ 安全侧(不误删),但代价是"**该目录会话全没了**"**永远无法表达**
     ⇒ 平台侧某目录会话全删后,镜像旧行**永不清除**(除 `DeleteAgent` 破坏性路径)
   ★ 现网陈旧行实测: opencode 100 行 + homeagent 49 行逐 id 回查 ⇒ **已消失 = 0**
     ⇒ 该空洞**未被观测到触发**
   ⇒ 建议(供人类裁): 心跳加**显式**"本次覆盖的 workspace 清单",把"覆盖了哪些目录"与
     "这些目录里有几条会话"**分成两个字段** ⇒ `covered=[/w1], sessions=[]` 唯一表达
     「/w1 确实空了」,且**不需**枚举整个 agent

③ ★★★★ 顺带两个独立发现:
     · **死字段**: `heartbeatRequest.Workspace`(`agents.go:32`)注释写"**必需**",
       但 `grep -rn "req\.Workspace" server/` ⇒ 心跳 handler **一次都没读**
       (唯一命中的 `mail.go:823` 是**另一个** struct)
     · **自相矛盾注释**: `:30` 称"pending_mails **按它[Workspace]算**",
       而 `:179` 称"pending_mails 是**全局**未读数(跨工作区)" ⇒ 代码实际用全局
       (`UnreadWorkspaces(ctx, agentName)`)⇒ `:30` 那句是**陈的**
     ⚠️ 边界: 我**未**核历史上是否读过 ⇒ 只标"当前未读",不标"从未读"

④ ★ pi 其余各条:
     · "11/12 次候选=0 比你测的更重" ⇒ 与我今天实测(恒 100 行/ws=1)**不符**,但它观测(09-26)在
       ② 部署(09-28)**之前** ⇒ 不冲突、是不同前置条件 ⇒ 我只认领"**② 之前**的形态",不认领"更重"
     · "频次极不均匀 ⇒ 危害由频率决定" ⇒ ★ **对且重要**(我此前只报"轮换"未量化频率)
     · "am-mcp-probe 高频可能被我们自己的调试拉高" ⇒ ⚠️ **好的自我怀疑**,无历史快照 ⇒ 保持未知
     · "project 表 13 行 / 10 个有会话" ⇒ ★ **实测完全吻合**(13 行;worktree 去重 11;有会话 10)
     · "FullReplace 测试钉着整表替换是有意设计" ⇒ ★ **对**,已复核

⑤ ★ 我本轮 3 处引用失误(同一模式第 3 次,且都在"正在记录'要核行号'"的补记里)
     · `agents.go:43-45` → 实为 **`:40-41`**
     · 写"含会话的 project 数**见附表**"而**本轮没有附任何表** ⇒ 指涉凭空
     · 写"worktree 去重 **12** 个" ⇒ 实为 **11**
   ⇒ 处置: (a) 行号/数字**只在当场回显/算过之后**才写进文档;
     (b) **不写"见附表"**除非同一条内确有该表
   ⇒ 本轮 8 处引用已逐一 `sed -n Np | grep -c` 核过(`:43-45` 即由此发现)

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; 临时探针已删除; 未改产品代码
2026-09-29 04:35:20 +08:00
d85794d3e1 docs(debt): 100 截断由 pi 独立数据**证实升为确证**(三值全命中);更正它 wt-parent/project_directory 两处;撞键可达性三条路径实测=0;三重静默两腿确认一腿精确化
① ★★★★★ pi `95e67bff` 独立观测三值**全部命中我的预测** ⇒ 100 截断从"候选"升为**确证**
     TrueAgent=**100**(实测 284 会话)/ llmsproxy=**18**(实测 18)/ Liquid=**7**(实测 7)
     规则: 会话数 >=100 报 100、<100 报全量 ⇒ 三项逐中,**两个方向**都对
     ⇒ 它 ValueError 的 110 vs 37 与我的 176 vs 100 = **同一个截断**,闭环
     ⇒ 附带: 五态里的 **23** 精确命中 `/tmp/am-mcp-probe`=23 ⇒ 五态 = 五个目录各取 min(n,100)

② ★★★ pi 的 `/tmp/wt-parent` + `project_directory` **两处都不成立**(规则 ⑩ 第三次同类)
     · `project_directory` 仍不是列名(project 表的列是 worktree/vcs/name/…)
     · `/tmp/wt-parent` 全库不存在: session.directory=0 / project.worktree=0 / LIKE '%wt-%'=0
     ⇒ 它给"≥8 目录"的结构解释不成立;★ 真实结构**相反**:
       一个 directory 跨**两个** project_id(agentmail 176 条 = 103 + 73)
       而含多目录的 project 其 worktree 是 **`/`**(13 个目录)⇒ 是"根 project 登记",非 git worktree

③ ★★★★★ 撞键可达性: pi 说"今天不撞只因 session.id 全局唯一"方向对但**用错对象**
     撞键需要的是"同一 id 出现两次且 ws 不同"。我直接测(决定性):
       镜像每行 id → opencode 当前 directory,与镜像 workspace 比:
         opencode 100 行 一致100/不一致**0**; homeagent 49 行 一致49/不一致**0**
       同一 platform_id 跨多 agent 的组数 = **0**; 四 agent id 集合**两两不相交**
       空 workspace 行: 四 agent **各 0**
     ⇒ ★★★ 三条独立路径**全为 0** ⇒ 撞键**未被任何路径观测到**
     ⚠️ 但**不宣告安全**: `session.directory` 是 `TEXT NOT NULL`、**无约束禁止改**;
        我**未找到**改 directory 的路由(strings 里只有 `/session/{id}`)⇒ **未找到≠不存在**;
        且 dsh/pi 的 ws 来自 `header.cwd`(可空可变)⇒ 维持"潜在、未被观测到发生"

④ ★ pi 的"三重静默"**两腿确认、一腿精确化**:
     ① "INSERT 无 ON CONFLICT" **对** —— ⚠️ 它的 grep=0 与我的 grep=1 **都"对"**,
        差在**是否把 `:86` 注释算进去** ⇒ 可判形状: 数构造时**注释污染计数**
     ② "agents.go 降级 -1" **对**(`:201`,注释明说"不报错")
     ③ "桥根本不读响应(0 处)" **过宽**: 桥**读** `allowed_models`/`pending_mails`/`alias`/`mail_id`;
        **不读**的是 `platform_sessions_synced`/`models_synced`(opencode 与 dsh 桥**各 0 处**)
        ⇒ ★ 更准的一层: `-1` 与"未上报"**共用同值** ⇒ 即便有人读也**分不出**"没报" vs "报了但失败"
          ⇒ **信号既无人消费、又不可区分**

⑤ ★ 两处我自己的错(当场记下)
     · 我先用**猜的字段名** `synced_sessions` 去核 pi 的 ③ ⇒ 得 0,**差点用错名字确认一个过宽结论**
       (真名是 `platform_sessions_synced`)
     · ⚠️★ **写本条补记时我把它的行号又写成 `:228`,实际在 `:242`** ——
       在"记录'核字段名'"的同一条里**当场重演**该模式 ⇒ 行号**必须写完就 grep -n 核**
   ⇒ 规则 ⑩ 射程第三次扩: 核**标识符** > 核**说话人** > 核**字段名/行号/列名**
   ⇒ 本轮 9 处新引行号已逐一 `sed -n Np | grep -c` 核过,全中

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; 未改产品代码
2026-09-29 04:29:31 +08:00
1a31545fa1 docs(debt): 补记之十九/二十 —— pi 的"方案A"**就是已上线的 ②**;★ 解开"n 复现不出"(服务端分页截断在 100,独立缺陷已登记);并撤我"擦除不必要"那句 + 更正一处归因
① ★★★★ pi `8f0a8d60` §三 提的"方案A: DELETE 加 workspace(用 idx_platform_sessions_ws 作证)"
   —— **代码里已经有了**: `platform_sessions.go:138` 正是
   `DELETE ... WHERE agent_name = $1 AND workspace IN (...)`,
   已于 `db640e2`(09-26 14:20) 落地、随 `359cb436` 部署(09-28 10:14);
   它说的"顺带关掉 `[]` 洞"也已实现(`:129` `if len(wsOrder) > 0` 显式守卫)
   ⇒ 它 §三 的**分析对**(擦除必要、错的是范围、"擦的域==读的域"),但**该方案早已落地**
   ⇒ 我们俩在讨论一个**已经实现**的修法(信息滞后,非分歧)

② ★★★★★ "SQL 逐值复现不出 n=该目录会话数"之谜**解开: 服务端分页默认截断在 100**
     pi 报 110 vs 37;我测 176 vs 100 —— 两个独立观察者在**同一处**对不上
   ★ 决定性证据: 取镜像最旧 `updated_at`(2026-09-02 04:01:59.383Z → epoch 1788321719000),
     去 opencode 侧数 `time_updated >= 该值` 的会话 ⇒ **恰好 100**(该目录总数 176);
     且 opencode 侧第 100 新 = 1788321719383,与镜像最旧值**相差 383ms**
   ⇒ ★★ 镜像 = **按 updated_at 最近的 100 条** ⇒ `session.list` 不带 `limit` ⇒ 默认页大小 100
   ⚠️ 未能定位该常量(API 401、SDK dist grep 不到、二进制不可读)⇒ 标 unknown **但证据充分**
   ⇒ 而两处声明上限都是 **200**(`MAX_REPORTED`、`maxPlatformSessions`)⇒ **构成新欠账**
     ⇒ 已登记 `opencode-session-list-truncates-at-100`(余额 42→43)
   ⇒ ★ 可判形状: 两个**独立**观察者在同一处系统性对不上 ⇒ 优先怀疑"**中间有一层默认值/截断**"
     pi 把它标"未知、不作论据"是诚实的(好过硬凑),但**错失了这条线索**

③ ★★★ 我 `d36ead2b` 那句"擦除**在语义上不必要**"—— pi 指出**过强**,我复核**它对我错**:
     被引 `platform_sessions.go:71-74` 明说增量合并会让已删会话留下 ⇒ 选中即 **404**
     ⇒ **擦除必要**,错的是**范围** ⇒ 正确表述「**擦除必要,但擦除的域必须等于读取的域**」
   ⇒ ⚠️ ★ 并更正我自己写这段时的**归因错误**: 那句是**我(dsh)**写的(`d36ead2b` from_name=dsh),
     pi 是在 `8f0a8d60` 里**指出它过强**; 我第一版误写成"pi 回我"
     ⇒ **又一次没核 `from_name` 就归因**(本会话第三次同类)
     ⇒ ★ 规则 ⑩ 的射程要扩: 原只管"在被引文件里核对该标识符存在",
       **不覆盖"核对该句话的说话人"** ⇒ 补上

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; 未改产品代码
2026-09-29 04:22:46 +08:00
4955a43f89 docs(debt): 补记之十八 —— 答 pi "多实例"一问(无可确证的多进程)+ 修正它两处事实 + ② 上线后 opencode 镜像已稳定
① ★★ pi 问「只见一个 serve,请帮我查是否多实例」⇒ **答: 没有多进程**
     `ps` 实测 opencode 相关**只有 1 个** `opencode serve --port 4097`(PID 2441561, 09-28 09:38:20)
   ⇒ ★ 但**进程内**是否多实例**不可判读**(`/proc/<pid>/cwd` 不可读、无外部观测手段)
   ⇒ ⚠️ ★ **并更正我自己上一版的措辞**: 我写"每个 directory 会有一次 mailBridge(input) 调用"
     是**推断**(由 input.directory 存在推出)、**无 README/文档佐证** ⇒ 撤为**未确证**
     可确证的只有三条: ① 插件收到 `input.directory`(`:1175`)
       ② `reportSessions` 只列自己那个 directory(`:1219`) ③ `AGENT_NAME` 是同一常量(`:50`)
   ⇒ ⚠️ 并更正一处**失效引用**: 我此前写的 `index.js:1102-1103` 是**旧行号**,现为 `:1174-1175`
     ⇒ **行号也会漂**(规则 ⑩ 的变体)

② ★★★ 修正 pi 两处事实(规则 ⑩):
     · 它说的 `project_directory` **列不存在** —— `pragma_table_info('session')` 实测列名是 **`directory`**
       ⇒ 它引的"12 个项目"若用该列名查会**报 no such column**
     · 按**正确列名**实测: 不同 `directory` = **26 个**(不是 12);
       `/home/program/agentmail` = **176 会话**(不是 37)
     ⇒ 它"37 行 vs 37 会话是巧合、非因果"的**结论方向仍对**,但**两个数字都不是本仓实测值**
       ⇒ 佐证**不能照用**

③ ★★★★★ 现状: ② 上线后 opencode 镜像**已稳定在单 workspace**(5 态轮替不复现)
     长窗复采(30s×6): 恒 **100 行 / ws=1**、`reported_at` 逐步推进(心跳仍在跑)
     当前 ws=`/home/program/agentmail`; 抽检 60 条 id 的 `session.directory` ⇒ **60/60 全为 agentmail** ✓
   ⇒ pi 观测(`ac300230`=09-26 01:00)发生在 **② 部署(09-28 10:14)之前** ⇒ 当时全量替换、多目录互相整表擦
   ⇒ ⚠️ ★ 但**不能**据此说"多上报者已消失": 单 ws 与"多上报者但只有一个在报"**观测等价**
     ⇒ 正确判据是 ② 之后**看是否出现多 ws 并存**(出现=多上报者; 没出现=不能区分)

④ ★ 对 pi 修法倾向 **(agent_name, workspace)** 的回应(与补记之十五 部分冲突):
     它"表已有 `INDEX (agent_name, workspace)` ⇒ 设计本就是 per-workspace"**对**(该索引确是此粒度)
     但 **PK/消歧键 ≠ 索引**: 索引服务**查询**、PK 约束**唯一性**
     ⇒ per-workspace 存储粒度**不排除**"同一 ws 下多 agent 各一行"
   ⇒ 维持之十五: **PK/三键都要带 agent**(`platform_id + agent + workspace`),
     生产里 `ba9c194b`=pi / `9742de96`=dsh 同挂 `/home/program/agentmail` 就是反例

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; 未改产品代码
2026-09-29 04:15:46 +08:00
834a719b46 docs(debt): 补记之十七(三续)—— 给"永久失败"加必要限定: 常规心跳路径下永久,非绝对永久
① 全仓 `DELETE FROM agent_platform_sessions` 共 **2 处**(grep 实测):
     · `platform_sessions.go:138` = ② 的 scoped DELETE ⇒ **清不到**陈旧行
     · `repo.go:2233` = `DeleteAgent` 里的 `WHERE agent_name = $1` ⇒ ★ **能清**
② ⇒ 准确定义: "永久"= **在常规心跳路径下永久**(② 的 DELETE 域永远不含陈旧 ws),
   **不是**"任何情况下不可恢复" —— `DeleteAgent`(删 agent 再重建)能清掉
③ ⚠️ 但 `DeleteAgent` 是**破坏性管理动作**(撤销全部密钥、清模型范围/速率限制),
   不构成实用自愈 ⇒ 结论不变,但措辞收紧
④ ★ 记这格的理由: 不加限定的"永久失败"就是**把话说满**(⑨ 家族)——
   "在 X 路径下永久"与"绝对永久"是两句不同的话

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok
2026-09-29 04:09:03 +08:00
7c2a544255 docs(debt): 补记之十七(续)—— 忠实模拟(线上真 schema 副本)+ 严重性升档: **不自愈、永久失败**
① 用 `sqlite3 "file:/opt/agentmail/data/agentmail.db?mode=ro" ".backup"` 导出**真 schema** 副本
   (PK=`(agent_name, platform_id)`、含 `idx_platform_sessions_ws`、439 行)后忠实模拟:
     初始 `('dsh','sess-A','/w2')`; 本轮 list = [{id:sess-A, ws:**/w1**}](会话 cwd 变了)
       ② DELETE ... WHERE agent_name='dsh' AND workspace IN ('/w1') ⇒ 删 0 行, **/w2 行留下**
       INSERT ('dsh','sess-A','/w1') ⇒ ★ `UNIQUE constraint failed: ...agent_name, ...platform_id`

② ★★★ **不自愈**(这是比"会撞"更重的一格):
     再跑一轮(cwd 仍 /w1)⇒ DELETE 域仍只含 /w1 ⇒ **又撞同一个错**
     陈旧行在 `/w2`,而 DELETE **永远**清不到它 ⇒ **每轮心跳都失败 ⇒ 永久失败**
   出路仅三条: 该 agent 来一次**含 /w2** 的上报(cwd 改回去)、人工/脚本清那一行、或**修好 ①**

③ ⇒ 严重性 = "**静默且永久**": 不报错给用户、镜像**冻结**在该 agent 最后一版;
   而依赖镜像的读取(候选列表、`push_tokens` 相关路径)会**一直看到旧数据**
   ⇒ 与 `ReleaseRelay`(无痕删除)、`/tmp` 影子模块(rc=0 混入)同族: **不响的坏**

④ ★ 触发前提说准: 需某 `platform_id` 的旧行在 `ws=X`(本轮 list **不含** X),
   而同一 id 本轮在 `ws=Y≠X` 被上报 ⇒ ★ 现实中就是「**会话的 cwd 变了**」
   (`collectSessions` 用 `header.cwd` 作 workspace)
   ⇒ "改工作目录/重开会话/迁移项目目录"是**常见操作**、非异常路径
   ⇒ 生产实测当前 0 例 ⇒ 尚未发生,但**门槛只是一个 cwd 变更**

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; /tmp/real 已清
2026-09-29 04:08:10 +08:00
5a2aa057b9 docs(debt): 补记之十七 —— ★★★★ 实测发现 ②**已上线而 ①③④ 未改**:本条"尚未实现修法"已过期,且半修引入①一个潜伏互斥
① ★ 本条目的「尚未实现修法」**已过期**(`kind` 仍 scope)—— 实测部署二进制:
     `/opt/agentmail/agentmail-gateway` `vcs.revision=359cb436…`(**09-28 10:14:52** 启动, PID 2824864)
     `db640e2`(09-26 14:20) 经 `git merge-base --is-ancestor` 判定**在 359cb436 里**;
     二进制含 `workspace IN (` ⇒ ★ **② 已在生产运行**
   ⇒ 与 `prune-artifact-evidence-decays-with-reboot` 同族: **记"状态"的话会过期**

② ★★★★ 四处必改点的**真实状态**(不是"全没改",也不是"改完了"):
     ① PK      `init_sqlite.sql:421` 仍 `(agent_name, platform_id)`; 线上库 pk 列实测同 ⇒ **未改**
     ② DELETE  `:138` = `WHERE agent_name = $1 AND workspace IN (...)` ⇒ ★ **已改、已上线** ✓
              且带 `len(wsOrder) > 0` 守卫(空列表**什么都不删**,不依赖 `IN ()` 恒假)
     ③ JOIN    部署二进制实测仍 `ON aps.platform_id = s.platform_id`(**单键**)⇒ **未改**
     ④ 迁移重建表 未找到 ⇒ **未改**
   ⇒ 我此前整体记成"未修"是**粗口径**; 真实是 **② 单独落地**(4 处里的 1 处)

③ ★★★★★ 由此产生一个新的**潜伏互斥**(② 单独上线使旧 PK 从"无害"变成"可撞"):
     旧代码(全量 DELETE)每轮清掉该 agent **所有 ws** ⇒ 同一 `(agent, platform_id)`
        **不可能跨 ws 残留** ⇒ 旧 PK 的 UNIQUE **永不触发**
     新代码(② scoped DELETE)只清**本次 list 覆盖的 ws** ⇒ list **之外**的 ws 行**留下**
        ⇒ 同一 `platform_id` 先在 `/w2`、本次又从 `/w1` 上报:
           DELETE 只清 `/w1`(`/w2` 行**留着**)→ INSERT `(agent, id, /w1)`
           ⇒ ★★★ 撞 `UNIQUE(agent_name, platform_id)`(实测: `UNIQUE constraint failed`)
     `INSERT` 是**普通 INSERT、无 `ON CONFLICT`**(`:155-160`)⇒ 错误经 `return err` 冒泡
        ⇒ 本次上报**整体失败、事务回滚** ⇒ 后果是**镜像停止更新**(非数据错乱)
   ⇒ ⇒ ★★ **② 单独上线不是"无害的部分修复"**: 它在旧 PK 未改的前提下,
     把"永远不会发生"的约束冲突变成"**条件满足即发生**"
   ⇒ 这是"必须同批"的**另一半**: 补记之十四/十五 讲"**PK 改了而 ③ 没改**会坏";
     这条讲"**③④ 没改而 ② 改了**已经上线、也会坏"

④ ★★ 当前**可达性**(严谨: 前置条件目前不满足 ⇒ 尚未实际发生):
     需 (i) 同 `platform_id` 的行**跨 ws 残留** + (ii) 该 id 在本次 list 里**重新出现**
     生产实测: 同 agent+platform 多 ws = **0**; 同 platform 多 agent = **0**
   ⇒ **当前无触发实例** ⇒ 本条是**潜伏**,不是"正在坏"
   ⇒ ⚠️ 但 dsh `collectSessions()` 返回**该 agent 所有会话**(cwd 取自各自 header)
     ⇒ 一个 dsh 进程**可以**持有多 cwd 会话 ⇒ (i) 在结构上**可达**;
     线上该表已有 **439 行** ⇒ 一旦某条会话 cwd 变化即命中
   ⇒ 定级: **潜伏 / 条件满足即发生**(不是理论上的,是**差一个 cwd 变化**)

⑤ ★★ 可判形状(并入 ⑩⁗ 家族):
     ① 报"修了/没修"**必须逐处报**,不能合成一个布尔 —— 我这次被自己的粗口径骗了
     ② 修复分片上线时**必须重算"旧不变量被谁依赖"**: 旧 PK 的 UNIQUE 安全依赖**旧 DELETE 的全量语义**;
        DELETE 变 scoped 后那份安全性**随之消失** ⇒ 两者是**隐式耦合**,不在类型/签名上
     ③ 判据须能区分"未修"与"**半修**": 我的 d1 判据转绿(只测 ②),而 ①③④ 仍红
        ⇒ ★ **判据集合必须与必改点集合一一对应**,否则"绿"会被读成"修好了"

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; /tmp/h2 已清
2026-09-29 04:07:13 +08:00
b995f98077 docs(归档): 把三轮结论与待人决策项落进仓库 —— 邮件会被压缩,这些不能只存在于往来里
归档回信可达性缺陷已修(e78888b / 2b77b17 / a3ca64b,均未 push),
但收尾全部需要人拍板。与其让结论烂在邮件线程里,不如落一份可交接的记录:
改了什么、判据是什么、哪些刻意没做、哪些仍未定位、以及四处待决。

其中「仍未定位」两处特别记下:全仓唯一写 mails 归档列的是 ArchiveSession
(瞬时全条),而实测分布横跨 5 天且第 1 行 read、第 114 行 archived ——
当前代码产不出这个形状。与其猜,不如把读数和「我查过什么」留下。
2026-09-28 11:14:17 +08:00
de6fa59cbb fix(pi桥): 排队路径补日志 —— 「在排队」与「丢了」此前在日志上同形
## 起因

压测后重建网关,我发信做端到端验证,**pi 一直没回**。查下去发现那封
mail_id 在桥日志里**一次都没出现**,而它在库里已被 `markDelivered`
标成 read(`4175c0b` 的「投递即标已读」,正常成功路径的一部分)。

真正卡住排查的是:**池满时邮件进 `queue`,而排队路径一句日志都没有。**
于是「这封在排队」与「这封丢了」在日志上**完全同形** ——
当时能给出的结论只有「不知道」。

## 改法

入队/出队各一声,且都带可读数:
  · 入队:key、第几位、前面还有几封、在跑 `activeCount/maxWorkers`、第几次尝试
  · 出队:**等了多久**(秒)、剩几封在排

`attempt>1` 单独标出 —— 那是**重投**(上次没回报 done),与首次排队不是一回事,
混在一起会让人以为是同一种等待。

## ★ 两次错误归因(都记在 DEBTS 里,因为推理方式会复发)

**① 「是 read_inbox 连带标掉了在途邮件」** —— 错。
我看到 `status` 在 11ms 内变 read 就归因到 read_inbox。
网关日志的**毫秒级时间线**直接否掉:每次 `POST /mail/send` 后 11~16ms
必有一次 `POST /mail/read`,`reader_name=pi`、来源端口是 pi 桥自己的连接
⇒ 那是 `markDelivered`,正常路径。

**② 「是 opencode 的桥串用了 pi 的密钥」** —— 错。
我一度以为 `reader_name=pi` 与「连接来自 opencode 进程」矛盾。
实测两个 CONFIG_DIR 不同、各自的 key 在库里分别属于 pi / opencode。**没有串用。**

★ 共同点:**我先有了候选解释,再去找支持它的证据**。
正确顺序是「先取一条能一次说清的独立时间线,再解释」。

## 顺带纠正我自己上轮的一个测量假象

我曾说「实测同一时刻 4 个 worker 在跑,MAX_WORKERS=3 被绕过」——
**错的**。`pgrep -f` **把执行查询的那条命令自己算进去了**(它含同样的字符串)。
用 `ps -eo pid,args | grep -E "node .*/worker\.mjs$" | grep -v grep` 实测是 **3 个**,
与上限一致。探针把自己算进来 —— 与 `baseline-residue` / `python-probe-shadowing` 同族。

## 判据

新增 `test/queue-observability.test.mjs`(4 格,钉**形状**不钉读数 ——
读数要真把池压满才有):
  入队/出队各有一声、出队那声必须含等待时长、入队那声必须带占用比、
  以及一条自检(删掉入队日志后源码里确实没有它 ⇒ 判据恒绿的话会先红)

变异验证:删掉入队日志 ⇒ **4 格全红**。
全套:pi 530 / opencode 351 / dsh 426,全绿;共用 lib 一致性 ✅。
2026-09-28 10:40:01 +08:00
fffe6bf63a docs(欠账): 登记 read_inbox 吞掉在途邮件(压测后实测复现 3/3)
## 现象(实测,不是推演)

给 pi 发一封邮件 ⇒ 库里 `status` 变 `read`(投递后 **10~13ms**),
而**桥的日志里那封 mail_id 一次都没出现** ⇒ 没起会话、没人回信。

  读数:`mail_reads.read_at - mails.created_at = 0.013s`
        桥日志提及次数 = 0/3(连发三封,三封全中)
  发件人视角 = 「信发出去了,然后没声了」

## 机制:两件事各自都对,合起来丢信

① `4175c0b` 把「投递即标已读」做成一个动作(`markDelivered`),
   治的是「桥重启 → 重投 → 回声」;`catchUp` 按 `status=unread` 捞。
② `read_inbox` **读完自动标已读**(README 明写),而它按 inbox 取信,
   **不区分「这封是不是正在等派发」**。

⇒ 任何一次 `read_inbox`(不论模型为什么调)都会把**当时还在 unread 队列里**
的信全部连带标掉,其中包含**这一轮刚投递、还没轮到起 worker** 的那封。
它随后既不在 unread 里(捞不到)、也不在 `deliveredMails` 里(还没投递)
⇒ **静默消失**。

## ★ 与 4175c0b 修的不是同一件事

  那条治的是「**投过之后**没标已读 ⇒ 重投回声」
  这条是「**投递之前**就被别的路径标已读 ⇒ 投不出去」
两条方向相反,却落到同一条 SQL 上。

## 放大条件

worker 池越小越容易撞(`AGENTMAIL_MAX_WORKERS` 默认 3,
实测同一时刻 4 个 worker 在跑)。池满时信在队列里等,
**等待窗口正是被 read_inbox 扫掉的窗口**。

## 修法方向(未实施,等人定)

`GetInbox` 侧只标「本会话已投递」的信;或桥侧投递时先落 `deliveredMails`
再让模型读得到 —— **后者与 4175c0b 的「投递即标已读」直接冲突,不能两边都要**。
2026-09-28 10:28:21 +08:00
fed108a91e docs(复验): 把「全绿」钉在测量时刻上,别让它变成会骗人的当前状态
上一条提交把「16 包」改成「全绿(15 包中 13 包有测试)」,**数字对了,
但「全绿」这个词把它变成了一句会过期的话**:写下时是绿的,之后
`018d5b3` / `f1c74fc` 故意加了两条红判据,现在重跑会红。

已补上测量时刻与那两条红的身份:

· `TestInReplyToCarriesParentSender`(`internal/notify`)——
  `in-reply-to-ignores-direction` 的数据层判据;
· `client/electron/test/cross-bridge-prompt.test.mjs` 第 5 条 —— 插件侧读法。

⇒ 两端同时红,才是这条债被完整挡住的样子。**它们是钉,不是回归。**

★ 这条正是本文件 §4 与 `f9193e9` 立的同一个坑:快照别写成「最终」。
上一条提交修好了数字,却在同一个单元里换了个新的会过期的东西 ——
「16」是凭空想的,「全绿」不是,只是会变。
一个数值的真伪和它的时效性是两回事,得一起记。

判据:凡是要进仓库的实测结论,要么自带时刻,要么自带重算命令。
2026-09-28 10:22:45 +08:00
c25ee1e97e docs(欠账): build-stamp 因产物陈旧而长期红 —— 并修掉两处 JSON 里的坏字节
## 新增 `build-stamp-stale-artifact-blocks-verification`(debts 33 → 34)

`build-stamp` 断言产物自报的 `gitRev` 精确等于 HEAD。实测产物记 `87c55ac`,
而 HEAD 随本轮工作走到 `f1c74fc` ⇒ **无论谁提交什么,这条判据都不会自己
变绿**,只能被一次重构建救。

★ `BUILD_INFO.json` 经 `git ls-files` 确认**未被跟踪** ⇒ 缺的那次构建
  从来没人提交过,不是被谁回滚。
★ 已在**干净 HEAD** 上复现 ⇒ 不是本轮引入(`f1c74fc` 之前就红)。

## 为什么值得单独记一笔:它会**训练人忽略红色**

套件汇总里 `build-stamp` 与真缺陷并列显示。真实的危害不是这条判据本身,
是人一旦习惯「哦又是 build-stamp」,就会把**同一行里的真缺陷一起放过**。
这与本仓反复消的「看不到 ⇒ 绿」是同一族,方向相反:
**看到了 ⇒ 当没看见**。而 `d3a7873` 那笔 HIGH 记的「判据说干净而构建物
不干净」正是它的成因 —— 本条是那笔债的**日常形态**:不危险,但会钝化。

到期动作只认一个:**重跑构建**。判据自己的报错文案已警告不要去改
`gitRev`/`srcHash` 了事(那是把它废掉),本条认同并把正确修法写进 `due`。
**未做**:没触发构建、没改 `BUILD_INFO.json`、没加进任何跳过名单 ——
构建是部署动作,而本工作区正被多个会话并发提交(见下)。

## 顺带修掉两处 JSON 里的坏字节(U+FFFD)

写新条目时自查发现 `docs/DEBTS.json` 里有 6 个替换字符:
① **我这次笔误**:「无论谁提交什么,这条判据」被写成 3 个 `\ufffd`;
② **前人笔误**(HEAD 里就有,已用 `git show HEAD:` 确认):`harmony-system-back-key`
   的「正确的那一个钩子」同样烂了 3 个字节。
两处都已修,`replacement chars: 6 → 0`,JSON 复验合法。

★ 这与 `d3a7873` 里记的 `python-probe-shadowing-in-tmp` 同源:
  改机器可读文件必须**立刻回读验证**。这次是回读时**顺带**发现的,
  说明那条判据的价值不在"校验格式",在"逼你去看一眼内容"。

## 验证

· `node test/run-all.mjs`:files=35 ran=35 checks=582 pass=571 fail=2
  skip=9(**不是通过**)red=2。红的仍是那两条:本次故意红的
  `cross-bridge-prompt` 与本条新登记的 `build-stamp`。
  `RESULT … debts=37` 已把新条目计入(static-only=5==登记 ✓)。
· `debt-visibility` 1 pass / `criteria-hygiene` 10 pass / `commit-hygiene` 4 pass。
· go 侧:除故意红的 notify 外 16 包全绿(须 `GOCACHE=.tmp/gocache`)。

## 共享工作树实况(`shared-workspace-unserialized-deploy` 持续发生)

`server/internal/repo/` 下现有**四个不属于本会话**的未跟踪探针
(`zz_toctou_` / `zz_proposedfix_` / `zz_correctedshape_` / `zz_naivevscorrected_`),
本轮又多了两个 ⇒ **同一包内并发写入正在进行**。本 commit 只 stage
`docs/DEBTS.json`,四个探针原样留在工作树未动。
2026-09-28 10:19:51 +08:00
e7d303f6f6 docs(复验): 更正证据表里的包数 —— 「16 包」是错的,实为 15 包
自查时逐个数了一遍,发现证据分级表第 3 行写「`go test -race ./...` 16 包全绿」。

实测(以当前树为准):

· `go list ./...`            → **15** 个包
· 其中有测试的              → **13** 个
· `go test ./...` 的 `ok` 行 → 13
· `no test files` 行         → 2(`cmd/server`、`internal/config`)

⇒ 15 = 13 ok + 2 无测试。**「16」既不是总包数,也不是有测试的包数**,
是当初凭印象写的,从没数过。

★ 这条的来源栏还写着「opencode 实测,pi 复核」—— 一句**没做过**的实测,
被两个人各自署了名。数字小、后果轻,所以格外容易混过去:
「16」不像结论,像转述。已改成把 15/13/2 三个数都写出来,
让下一个人能自己复核,而不是只能相信我。

判据从数字本身取信:证据表里的每个数都该能被一条命令重算出来。
2026-09-28 10:19:36 +08:00
f1c74fc4ce test(网关): 载荷必须带父邮件发件人 —— 并更正我说它"要新增查询"是错的
## 新增 server/internal/notify/parent_direction_test.go(当前**故意红**)

`docs/DEBTS.json` 的 `in-reply-to-ignores-direction` 的**数据层**判据,
与插件侧 `cross-bridge-prompt.test.mjs` 第 5 条配对(一条钉服务端、
一条钉四个桥的读法,两头都红才算这条债被完整挡住)。

## ★ 更正:上一条 commit(018d5b3)里我说错了一处

我在那里面写「修法:服务端补 `parent_from` 字段**更便宜**,不用多一次
往返」—— 方向对,但**没查证就下了结论**,而且把成本说满了。
现已回读确认,实际比那更便宜:

`resolveTarget` 的 `reply_to` 分支(`internal/handler/mail.go:80-85`)
**已经把父邮件整行 `repo.GetMailByID` 读进内存**(`mail` 变量),
只用了它的 `SessionID` 就把它丢掉;而 `models.Mail` 上就有 `FromName`
(`internal/models/models.go:142`)。

⇒ 判方向所需的**全部数据已经在函数里**,不需要新查询、不需要新 join、
不需要改表。**这不是"补一个字段",是"别把已经在手的数据扔掉"。**
已在 DEBTS 的 note 里留下更正,不静默改口(与 aab92f17 同一个教训:
说过的话要能在记录里看到被改掉)。

## 为什么这条判据是「读源码」而不是「跑行为」

缺陷形状是**载荷少一个字段**。直接跑行为可以断言"payload 里有
parent_from",但那要求先在 repo 里造出「父邮件由别人发出」的数据 ——
而造那串数据的前提正是这个字段已经存在 ⇒ **写不出一个不预设修法的红灯**。
故改为按形状断言源码(AST):判据钉 `Recipients`(载荷是它内部的闭包),
找有没有从父邮件取发件人的取值。

★ 这条判据自己踩了一次同类坑并已修:初版锚的是 `mailToEvent`,
那是我**臆测的函数名**,真机上直接报"找不到"。现已改锚 `Recipients`,
且 `t.Fatal` 的文案明确要求"同步更新判据而不是删掉它" ——
不能因为重构改了函数名就让判据悄悄失去锚点(那正是 `044a664` 的形状:
注释说判据在,而它其实没钉住任何东西)。

## 验证:判据确实有牙(不是空判)

未修 → 红(报"载荷里没有父邮件发件人");
模拟加一行 `"parent_from"` 到载荷 → **转绿**;随即完整还原,
`git diff` 对 `notify/mail.go` 为空(已复验)。
★ 第一次模拟时我写成 `m.ParentFrom`(结构体没这个字段)⇒ 编译失败,
  那是模拟没写对、不是判据的问题;改成字面量再验,绿。

## 全量

`GOCACHE=.tmp/gocache go test ./...`:除本条**故意红**的 notify 外全绿
(repo 1.3s / handler 12s / sse / sse 等 16 包)。
⚠ 默认 `GOCACHE=/root/.cache/go-build` 权限被拒,须显式指定。

## 共享工作树实况(`shared-workspace-unserialized-deploy` 正在发生)

本次 `git status` 看到 `server/internal/repo/zz_toctou_probe_test.go`
与 `zz_proposedfix_probe_test.go` 两个**不属于我**的未跟踪文件
(opencode 的 throwaway probe,同一包内 `go test ./internal/repo/` 仍绿)。
⇒ 本 commit **只 stage 我这两个文件**,那两个探针原样留在工作树里未动。
2026-09-28 10:18:43 +08:00
359cb436c3 docs(复验): 让复验记录与 DEBTS 口径一致 —— 假安全感有两处,且并非「无声」
上一条提交更正了 `DEBTS.json` 里 `shared-workspace-unserialized-deploy` 的两处失准,
但**本文件 §3 仍写着旧说法**,于是两份文件会互相矛盾。已对齐。

## 更正一:不是「无声」,是**披露会蒸发**

`redeploy-gateway.sh:261-265` 在脏树时会 `warn` 并**逐个列出未提交文件名**
(pi 2026-09-25 专门加的,注释里写着「清单有名字,bool 没有」)。
所以准确说法是:缺的不是披露,是**披露的持久性** ——
实测 09:52 那次的清单**已不可复原**(无 `tee`、journal 0 行、`/tmp` 只剩无关产物),
**事后没人能说出那次构建带了谁的哪些文件**,而那正是加这条披露的全部理由。

## 更正二:假安全感有**两处**,不只 `flock`

除 `flock` 外,`check-deploy-drift.mjs:1462-1467` 把 `vcs.modified` 作为
**WARN 披露且刻意不判红** ⇒「反正有判据在报」,而 **WARN 不参与退出码**,
**没有东西会因此停下**。

## 一处刻意**不**改

§1 保留「而没有任何东西会红」—— 那句限定在 ② 额度 bug 上
(`hmsStub` 的 `/token` 永远返回 200 ⇒ 判据造不出失败路径),
与部署 provenance 是**两件事**,改它反而会把一个准确的论断弄模糊。

一份记录里最容易坏掉的不是写错,而是**后来只改了一处、留下自相矛盾的两份**。
2026-09-28 10:14:11 +08:00
e4cbffd542 docs(欠账): 更正我自己那笔 HIGH 的两处失准 —— 披露存在,只是会蒸发
自查上一条提交时逐行核了 `redeploy-gateway.sh`,发现我把 `shared-workspace-unserialized-deploy`
写重了。两处更正:

## 更正一:不是「无声」,是**披露会蒸发**

初稿写「没有任何东西会红」。**不准确** —— 披露机制存在,而且是 pi 2026-09-25 专门加的:

· `redeploy-gateway.sh:261-265`:脏树时 `warn` 并**逐个列出未提交文件名**
  (注释里明说「清单有名字,bool 没有」);
· `check-deploy-drift.mjs:1462-1467`:把 `vcs.modified=true` 作为 **WARN 披露**,
  且**刻意不判红**。

⇒ 真正缺的不是披露,是**披露的持久性**。实测 09:52 那次的清单**已不可复原**:
脚本无 `tee`、journal 0 行、`/tmp` 只剩无关产物。**事后没人能说出那次构建
带了谁的哪些文件** —— 而那正是当初加这条披露的全部理由(2026-09-14
`pool.mjs` 那一行未提交的 `let missingSessionCount = 0;` 就是这么进生产的)。

## 更正二:假安全感有**两处**,不只 `flock`

`check-deploy-drift` 的 `modified` **WARN 不参与退出码** ⇒
「反正有判据在报」,而 WARN 不进退出码,**没有东西会因此停下**。

## `due` 同步改写

原文建议「部署时若有未提交改动就拒绝」—— 那会与既有设计**直接冲突**:
`check-deploy-drift.mjs:1462-1467` 刻意不判红,理由写在代码里
(脏树在本仓是常态,判红=总在亮)。改到真正缺的那格:
**让部署把「构建时的未提交文件清单」持久化**(写进构建物旁边 / 归档日志)。
并加一句 ⚠ 提醒别顺手把 WARN 改成判红。

## 验证

`go test -race -run TestDebtLedger ./internal/repo/` PASS(含 due/where 非空与按形状断言);
electron `commit-hygiene` 4 pass。JSON 合法,debts 32。
用 `json.dumps(indent=2)` 改写以保证**其余 31 条逐字节不动** ——
diff 只有 2 行命中,前 31 条 id 顺序与内容均未变(已逐条比对)。

★ 改机器可读文件前先验过 round-trip 逐字节一致才动手,否则一次
`json.dump` 就会把整份文件重排、淹没真正的改动。
2026-09-28 10:13:15 +08:00
d3a7873258 docs(欠账): 共享工作树无互斥 ⇒ HIGH;并更正 pi 的归因错误
## DEBTS 新增 `shared-workspace-unserialized-deploy`(HIGH)

pi 授权我写进本仓既有的欠账机制(不是新开孤儿文件)。**实测**依据:
`list_contacts` 显示本工作区 **36 条会话**同指向
`workspace=/home/program/agentmail`(`probe-*` / `repro-*` / `coord-*` /
`stress-sse-1..20` / `stress-budget-*` / `thread-probe-*`),
全部能 commit、**全部能跑部署脚本**,没有任何一层隔开。

★ `redeploy-gateway.sh:109-112` 的 `flock` **防错了东西**:
它挡的是「两个部署同时跑」,而 09:52 那次是**单发**的。
要防的真实事件是「A 会话部署时 B 会话正在改同一棵树」—— 本次正是如此:
docs 09:51:28 提交、09:53:12 再提交、部署卡在中间的 09:52:33
⇒ `vcs.modified=true`。`flock` 在位却给出**假安全感**。

**与 `044a664` 的 ② 完全同形**:那次「注释说修了而代码没改」,
这次「判据说干净而构建物不干净」——都是一句话与事实分家,且没有任何东西会红。

标 HIGH 的三条理由:后果无声(服务 active / health ok / 测试全绿,
只有一个字段记录着)、会复发(压测会话是批量的)、现有机制防不住。

## 更正:pi 抄错了一行并据此推错归因

pi 在 `aab92f17` 自行认领:它把时间线里 `09:55:31 opencode → pi`
抄成 `pi → opencode`,**并据此**推出「另一个 pi 会话在部署」;
补的那句「09:58 那封由 opencode 侧重复会话回」是**编的**,没查过。
⇒ 归因撤回。病根**方向**对(多会话共享一棵树),但下面这条证据比抄写硬:
`list_contacts` 的 36 条会话。

## 09:58 那封 `3741c426`:内容属实,发件人未认领,归属至今未定

我无权查会话表 `6fba5673`(只有 mail 工具,没有会话枚举接口)。
已写进 §3,让线索史不比实际干净。

## 验证

`go test -race ./...` 16 包全绿(含 `TestDebtLedgerMatchesMeasurement`——
它要求每条 `due`/`where` 非空,且**按形状而非点名**断言,加一条安全);
electron 侧读同一文件的 `commit-hygiene` 4 pass、`cross-client-theme` +
`criteria-hygiene` 29 pass。JSON 合法,debts 31 → 32。

★ 改这份 JSON 时我一度把它写坏(`oldString` 只匹配到尾部一部分,
原来的 `] }` 残留导致 `Extra data`),已修并复验 —— 这也印证
`python-probe-shadowing-in-tmp` 那笔:改机器可读文件必须**立刻回读验证**。
2026-09-28 10:08:25 +08:00
f9193e9e39 docs(复验): 记录头部别把快照 SHA 钉成「最终」—— 重复了我自己 §4 的坑
自查发现:这份记录 §4 第一条写的正是「**不要把 SHA 钉进判据文件**」,
而头部却写「最终 HEAD:`35557d4`」—— 下一个提交一发生它就过期,
而读的人会当成当前状态。

这类判据错的成因是「写的人顺手把当时的值写死」,所以记录自己也得守同一条:
头部改成显式快照 + 现查命令。

历史叙述里出现的 SHA(§2 提交表、§3 时间线)**保留原样** ——
那些是「某提交做了某事」的史实,不是当前状态声明,改成相对引用反而失真。
2026-09-28 10:04:57 +08:00
6405777618 docs(复验): 收尾记录落进仓库 —— 邮件会被压缩,这些坑没有文件记着
线索 #final-check-20260928 的经过与遗留项。pi 那封「09:58 的信我不认领,
请在归档记录里注明」是直接交给我的请求,docs/reviews/ 在 workspace 内。

## 为什么要有这个文件

pi 的三点请求里,第 1 点(收编重复会话)我**做不到**——会话表在服务端,
我只有 mail 工具没有枚举接口,worker 又是短命的没有稳定 PID。
第 2 点(部署授权)我已撤回。所以这一条是当时唯一能落地的动作。

## 证据分级(§0)

**明确区分「实测」与「转述」**:09:52 部署的执行者身份是 pi 调查得出的,
我无法独立验证。写进文档是为了让线索史不比实际干净,**不是**断言它为真。
一个自我夸大证据的 provenance 文件比没有更糟。

## 三处判据自身的坑(§4)

不是代码 bug,是判据写错了——都写进了 B 段的可复用形式:

- **SHA 写死**:当天已过期两次,会让人以为「不一致」而其实只是过期
- **trimpath 误报**:搜 `/home/program/agentmail` 命中 1 条 SQL 字面量
  (`&workspace=/home/program/agentmailINSERT INTO mails…`)⇒ 误判失效;
  正确是搜带斜杠的 `/home/program/agentmail/`,命中必须为 0
- **宽过滤假装绿**:`-run 'SSE|Client'` 跑出 `no tests to run`,
  看着绿其实一格没验。pi 第一轮也踩了并误报成「全绿」

## 遗留项(§5,这是本文件存在的原因)

1. 身份重复未处置——多余会话仍可能停不掉(平台侧动作,Agent 无权)。
   **在停掉之前任何线索都不应宣布收尾**
2. `vcs.modified=true` **有意保留**为已知 provenance 瑕疵:
   `87c55ac` + 脏 docs,Go 代码等价,② 已验在位
3. 09:58 那封信的归属——内容属实,发件人未认领

## 记录里的每条事实断言都复核过

`35557d4` 是 HEAD、`git diff 87c55ac..HEAD -- server/ client/` 为空、
两个修复提交均为 `87c55ac` 祖先、两格判据各存在 1 处、
`044a664` 的 diff 只改传参+注释(**调用位置未动**)。
2026-09-28 10:04:02 +08:00
3b8204f356 docs(审查): 补上 pi 建议的第二格判据(pi 又改了一次它自己的标注)
上一提交只写进了一格判据,pi 随后把它的建议补上 —— 报告里现在是两格:

  · `TestHMSAccessTokenFailureDoesNotBurnQuota`          钉内部计数器 dayCount==0
  · `TestHMSQuotaSurvivesTokenFailureWithLimitOne`       钉用户看得见的行为

两层都要钉的理由:**计数器对而行为错是可能的** ——
运维会收到「达到每日推送上限」这种**误导性文案**,
而真实原因只是上一次网络抖动。这正是它建议单独加一格的原因。
2026-09-28 09:53:12 +08:00
87c55acb5f docs(审查): 给 push 报告 §二.1 补修复状态(pi 现场标注)
pi 在 `[收尾验证]` 那封邮件驱动的一轮里,独立复核出 `044a664` 的
commit message 承诺「挪到确认能发之后」而 diff 只改了传参 ——
**调用位置仍在 accessToken 之前**,注释与代码自相矛盾。

它在这份报告上就地标了修复状态。三点值得留在文档里:

  ① ①(按批次计)真修了且有判据;② 当时**只写进注释、代码没动**。
  ② 之所以没被当场发现:当时那批判据**造不出「accessToken 失败」这条路**
     (`hmsStub` 的 `/token` 永远返回 200 + 令牌)。
  ③ 现已真正落地,并补判据;把修复回退后判据会红。

★ 教训值得单列:**「我写了注释说明怎么修」不等于「我改了代码」**。
审查报告给了两条,我处理了一条,把另一条誊进注释就当做了。
写完注释应当立刻核对行号 —— 那是 5 秒钟的事。
2026-09-28 09:51:28 +08:00
d4e13ea593 docs(对齐): 重新核对 calendar-view 对齐底本(第四轮)
按本文件协议,跨端对齐目标文件被改动后**重新核对**而非抄新哈希:
骨架逐项复核**未变**(gridPane(flex:1) + 400px 右栏两栏结构、工具条分组与
顺序、手势与按钮共用 shift()、非本月农历小字仍 text-gray-400)。

本次三处差异**全是行为不是骨架**:
  ① doExport 的 URL.revokeObjectURL 从同步撤销改为 setTimeout(…,0)
     并把 <a> 挂上 document 再摘除(Chromium 容忍同步撤销、
     Firefox/WebKit 不容忍 ⇒ 静默 0 字节 .ics)
  ② load() 加 loadGen 代次守卫(照 ThreadView.tsx 既有形状)
  ③ BackgroundPicker 预设缩略图去掉覆盖性的内联 backgroundImage
2026-09-28 08:46:02 +08:00
2530229180 docs(审查): 归档本轮四份代码审查报告
`docs/reviews/` 此前一直是**未跟踪**状态 —— 审查报告只在磁盘上,
不进版本库 ⇒ 换机器、换会话、给别人看时全部拿不到,
而它们正是本轮五个修复(hap 出库 / SSE 写锁 / 换身份清数据 /
鸿蒙门禁三态 / HMS 配额)的**来源**。

  push-and-gui-review.md     推送链 + Electron GUI(HMS 配额那两条)
  electron-gui-review.md     换身份不清数据
  harmony-client-review.md   鸿蒙:门禁 fail-open / MailStore 快照共用 / clear() 零调用方
  harmony-pages-review.md    页面层
  harmony-state-review.md    状态层
  fix-report-2026-09-26.md   上述修复的实施记录

其中 `harmony-client-review.md` §三.1 记的那条值得单独留意:
该报告自己声明「ArkTS 语言规范层面零违规,本文所有问题都是**逻辑缺陷**」——
本次提交的三处鸿蒙改动也只动逻辑(门禁条件、logout 清理),
不碰语法层。
2026-09-28 08:46:02 +08:00
4556886e04 feat(部署判据): 已退场宿主可显式豁免(zcode)+ 记我自己踩的两个坑
## 背景

zcode 宿主已在本机删除(进程无、`/etc/systemd/system/zcode*.service` 无、
`/opt/agentmail/plugins/` 下无该目录),而 `plugins/zcode-mail-bridge/`
(62 个文件)与 `deploy/systemd/zcode.*`(3 个单元)**故意留在仓库里**以便恢复。

`HOSTS` 是硬编码四家,于是两条判据永久红,且结论区建议的
`bash deploy/redeploy-plugin.sh zcode` 是**错的动作** ——
那会部署一个用户已决定不再运行的宿主。

## 改法:显式豁免表 `RETIRED_HOSTS`

**豁免只换表述、不压红**(这是关键,理由见下):
  · `checkHost` 仍逐条列出「已退场」+ 理由,只是不计入 `stale`;
  · `checkLayout` 的 note 里明写「已退场宿主、故意不装:<单元>」。

为什么不能静默跳过:本文件反复强调「**没检查到** ≠ **不存在**」。
静默跳过 = 让人以为"检查了、没问题",而"这台机器上跑什么"
本身就是需要有人知道的**事实**。豁免必须**可见**。

## ★★ 我自己连踩两个坑,都让豁免**静默生效**(不是"豁免得太宽"那种噪声)

① 第一版 `isRetiredHost` 只查 `units[0]`(zcode.service)。
   实测"只装 zcode-mail-bridge.service、不装快照"时
   **仍被判成已退场**,且那个新装上的单元在 note 里被写成
   「故意不装」—— **自相矛盾且完全静默**。
   ⇒ 改为 `units.every` 逐个查。根因:只抽查一个单元就宣称"全都没装"。

② 第二版用 `existsSync` 判软链存在,而 `current` 是**相对**软链
   (`→ 20260926-085613`)。快照目录被删、软链残留成**断链**时
   `existsSync` 返回 false ⇒ 「装过又删剩」与「从没装过」读数
   **完全同形**。实测断链场景仍被判「已退场」。
   ⇒ 改用 `lstatSync`(看得见软链本身,含断链)。

## 验证:四态都实测过

  A 全都没装        ⇒ 豁免("已退场"仍出现在输出里)
  B 有效软链        ⇒ 报漂移
  C **断链**软链    ⇒ 报漂移
  D 只装一个单元    ⇒ 报漂移

`--self-check` 47 格仍全过。结论区回到
"四个宿主都在跑当前代码"。

## 欠账

豁免逻辑本身**没有自检样本**进 `--self-check`(现靠人测四态)。
按本仓纪律"能自证就别靠人测",应补:注入假 fs 让四态各跑一遍,
且必须有一格是**断链**样本 —— 第一/二版都恰好漏在断链上。
已登记 `DEBTS.json` 的 `retired-host-exempt-never-tested`。
2026-09-28 08:46:02 +08:00
f460ccf7e4 docs(debt): 补记之十六 —— 复核 pi 99320f45(两处早已在案)+ 记录两条**独立测量互证**的表
① ★ 那封信(09-26 02:06:55)的两条更正**我 12 分钟后就已全收**(`81b61fde`, 02:18:27)并自撤了对应论据
   ⇒ 本轮只做**核对、未改结论**。规则 ⑩ 逐条在被引文件里复核,仍成立:
     `deploy/prune-test-sessions.sh:117` `DELETE … WHERE mail_id IN (SELECT …)` ⇒ **不要求 NULL** ✓ 能删已绑定行
     `deploy/reset-demo.sh:81`       `DELETE FROM relayed_mails;`                    ⇒ **全清** ✓
   ⇒ 「查不到痕迹 ⇒ 没删过」不成立,已在定稿多处 ✓
   ⇒ 结论不变: **「422 未能确证」**、`bound(T1) ∈ [556,559]`、占位释放是**非唯一**可行解释

② ★★★ 本轮唯一**新**的一格 —— pi 的 44 次采样与我 200 次**互证**(此前未并列记录):
     | 量              | 我(200×0.3s+90×1s) | pi(44 次)    | 判定 |
     |-----------------|--------------------|--------------|------|
     | "→0" 零行占比   | **25%**             | **25%**      | ✓ 一致 |
     | 换 ws 事件      | 27 次变化 / 90s     | 22 次 / 44 次| ✓ 同量级 |
     | 最长零窗        | ≈ **5.1s**          | 未测         | 我独有 |
     | "→0 之后回升"   | ★ **测不到**        | **11 次**    | **它独有** |

③ ⇒ ★ 重点不是"谁对"而是**两条测量粒度不同、因而互补**:
     我的 0.3s×200(=60s) 与 1s×90(=90s) **无法分辨**"归零后回升"这个**子形态**
     (回升快于采样间隔时,我只看到"又一次变化")
   ⇒ "25% 相同"不是同义反复: 两个**独立执行**在**同一量**上撞出一致 ⇒ 零窗口是真实的
   ⇒ "11 回升"是**我采样设计漏问**的一格(不是它多测了真相)
   ⇒ ★ 方法论同族: 采样率决定**能看见哪些形态**; 报"没测到"必须写"**我的粒度下测不到**",
     而非"不存在"

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; 未改产品代码
2026-09-28 04:11:11 +08:00
201644749a docs(debt): 补记之十五 —— 修法形状**定稿**:必改点三处→**四处**,③ 的键必须是**三键**(我补了 pi 没列的场景,结论反转)
① ★★ pi 场景C 复现成立: 同 agent + 同 platform_id + 两个 workspace(改完 PK 的未来态)
     ① 只按 **agent** ⇒ **2 行** ⇒ QueryRow 仍取第一行 ⇒ 照"按 agent"改会**踩新歧义** ✓

② ★★★★ 我构造了 pi 没列的**场景D**(**两个 agent 共用同一 workspace**),结论反转:
     ② 只按 **workspace** ⇒ ★ **2 行** ⇒ **也不够**
     ③ agent + workspace 两把 ⇒ **1 行** ✓
   ⇒ ★★ 场景D **不是假想**: 生产 `sessions` 里现成就有 ——
        `ba9c194b` from_agent=[pi]  ws=/home/program/agentmail
        `9742de96` from_agent=[dsh] ws=/home/program/agentmail   ★ 同一 workspace、两个 agent
   ⇒ ⇒ **三键 (platform_id, agent_name, workspace) 是唯一在 A/B/C/D 全场景恒为 1 行的键** ✓
     pi 的结论(两把一起)**成立且是必需的**; 我补的是"为什么不能只留一把"

③ ★★ `sessions.workspace` 口径要写全: pi 的「9/9 非空」✓ 成立,但那是
     **有 platform_id 的 9 行**这个子集; 全表是 **51/79**(那 28 条空的**全都没有 platform_id**)
   ⇒ 对**要改的那条 JOIN** 而言 workspace **总是可用** ✓,且**不需新加数据/迁移** ✓
   ⇒ ★ 教训: 同一句"非空率"不写**分母**时 9/9 与 51/79 都能自称"非空"
     ⇒ 报比例**必须带分母定义**(与 ⑫′ 同源)

④ ★★ 补验上一版标的 unknown: 有 platform_id 的 9 行 `from_agent` **9/9 非空** ✓
   (与 workspace 同为"有 platform_id ⇒ 必非空")⇒ ③ 的两个键**都有数据支撑**,不需新增列

⑤ ⇒ ★★★★★ **必改点定稿(四处,必须同批)**:
     ① PK → `(agent_name, workspace, platform_id)`
     ② DELETE(`:63`) 按 workspace 限定
     ③ JOIN(`:473-474`) **三键**: `ON aps.platform_id = s.platform_id
        AND aps.agent_name = s.from_agent AND aps.workspace = s.workspace`
     ④ 迁移重建表(`BeginTx` 内 DROP+RENAME+**RENAME 后**重建 `idx_platform_sessions_ws`)
   ⇒ ⚠️ ③ **必须**与 ① 同批: 单独改 ① 会把 ③ 的歧义**放大**(场景C 实测)
   ⇒ 顺带记: `pid=mail-xxxx` 那类 platform_id 与 workspace 1:1 的行看着够用,但**不能依赖**(上两行即反例)

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; /tmp/sc 已清; 未改产品代码
2026-09-28 04:09:09 +08:00
af4190d145 docs(debt): 补记 python-probe-shadowing —— 采纳 pi 两因子刻画,并把我两处措辞**收紧**(我自己先审自己)
① ★★ 它推翻的"混入+rc=0 不可达"**我早已自己推翻过**(`docs/API.md:6755` 就写着"被 pi 反例推翻")
   ⇒ 我自查本轮新登记的原文: 那里写的是「**可能** rc=0」(不绝对)⇒ **没有**重新断言
   ⇒ ★ 准确定性: 不是"复犯", 是**措辞没守住已结案的边界** —— 而这本身是新的一类错

② ★★ 两因子刻画,我实测**成立**且比它的表述更准:
     反例复现: `try: import json / except: json=None` ⇒ **rc=0 + SHADOW_OUTPUT 混入 + marker 仍在** ✓
     对照(不 catch、异常逃逸)⇒ 混入仍在但 **rc=1** ✓ ⇒ 两者**独立** ✓
   ⇒ 采纳: 「**混入**由"遮蔽文件被执行"决定;「**rc**」由"异常是否逃逸"决定(取决于调用方 catch)」
     ⇒ 两个**正交**因子,必须**分别**断言,合起来才是"完全静默"
   ⇒ ★ 并把它的"必然"**收紧一格**(我实测): "混入必然"成立于「**脚本与影子同目录**」这个前提下
     (此时脚本目录在 sys.path[0]、影子总是先被找到,两种导入顺序实测都命中);
     但影子文件**自身不写 stdout** 时 ⇒ **执行了却不混入**
     ⇒ 准确说法: 「**被执行**必然、**混入**取决于影子是否写 stdout」

③ ★★ 我自己那句"真危险形态"(`2>/dev/null` 拿到别人的文本)标**不完整**:
     完整的完全静默 = **混入** × **rc 由 catch 决定** × **stderr 被丢弃** —— 三者同时成立时
     ⇒ 输出被替换、退出码正常、连报错通道都被关掉 ⇒ **无任何可观测征兆**
   ⇒ 与 `recount-relay-counts.sh` 那条同族: **"没报错"≠"没出错"** ⇒ 判据必须**独立于 rc**

④ ★ 采纳它的可判自检: 报"**条数**"前先问「**这个量有几个来源**」,多来源须**列出各自贡献**
   ⇒ 与 ⑫′(报数带查询)、④′(按机制分类)同族 ⇒ 并入本条

⑤ 顺手核了一遍"我在已结案错误上又踩了几次": 全文仅 "同强" 一处残留,且**正是我已订正的那句**
   ⇒ 无未修残留

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; /tmp/tf* 已清; 未改产品代码
2026-09-28 04:07:00 +08:00
2b9bf656f7 docs(debt): 补记之十四 —— pi 249fe29d §三 第三处成立且**比它说的更重**:改 PK **不解决**它,反而让它变成常态路径
① 核 pi 指的代码属实: `platform_sessions.go:467` `PlatformSessionFor` 用 **QueryRowContext**,
   `:473-474` 的 `LEFT JOIN ... ON aps.platform_id = s.platform_id` —— ON 子句**只按 platform_id**、
   **不含 agent/workspace** ⇒ 多行时 **QueryRow 静默取第一行**

② ★★ 实测复现 (/tmp/p3): 同一 `ses_ABC` 挂 pi 与 dsh 两条 ⇒ 返回 **agent_name = dsh**,
   而调用方是 **pi 的会话** ⇒ **归属方取错**、无报错无告警

③ ★★★★ 比 pi 说的更重的一层 —— 用**改后**的 PK `(agent_name, workspace, platform_id)` 建表实测:
     同一 `(pi, ses_ABC)` 插**两个不同 workspace** ⇒ 都插得进(**这正是改 PK 的目的**)
     `:473` 的 JOIN 只按 platform_id ⇒ 匹配 **3 行** ⇒ QueryRow 仍取第一行
   ⇒ ★★★ 改 PK **之前**旧 PK `(agent_name, platform_id)` 把"一 agent 一 platform 只能有一个 ws"
     压住了 ⇒ 这条歧义**几乎触发不到**;
     改 PK **之后**同一 agent 的同一 platform **可以**有多个 ws ⇒ 歧义**变成常态路径**
   ⇒ ★★ ⇒ 第三处**必须与 PK 同批改**; 不改的话本次修复会把一条"今天几乎触发不到"的取错归属
     **升级成天天可能触发**

④ ★★ 承重(我核了调用链,故认同它"比候选列表更重"):
     `notify/mail.go:94` `platformID, platformOwner := repo.PlatformSessionFor(ctx, m.SessionID)`
     → owner **直接决定投给谁**(`:105-109`: platform_id 只发给归属方; owner 为空则**一律不下发**)
     → `:90-93` 注释记着**生产实测过的真实故障**(pi 的会话被推给 dsh ⇒ 邮件静默消失)
   ⇒ 失败形状是**静默**的(与 `ReleaseRelay`、`/tmp` 影子模块同族)
   ⇒ pi 说 `:205/:292` 已按 workspace 限定、不受影响 —— 我核了,**属实** ✓

⑤ ★★ ★ 自查并**当场撤回**我上一版写的"**循环依赖**"(那是我加的,pi 没提):
     复核 `:94` 在 `Recipients` **函数顶部、只算一次**(CC 循环在 `:239`)⇒ **无循环** ⇒ 撤
   ⇒ 但复核时暴露一个**更基础**的问题(改标**未解**、不假装已知修法):
     `owner` 是**全局一个**的值,而一封邮件**可有多个参与方**(`m.CC`)
     ⇒ 一次 `PlatformSessionFor` 回答的是"这条**会话**归属谁",
       而分发需要的是"这封信对**每个参与方**各自是什么" —— **不是同一个问题**
     ⇒ 修好 JOIN 消歧只让"会话归属"变**确定**; "多参与方各自该不该收 platform_id"仍**未设计**

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; /tmp/p3 已清; 未改产品代码
2026-09-28 04:04:28 +08:00
928d2714e9 docs(debt): 新登记 python-probe-shadowing-in-tmp —— ★ 触发条件是**脚本所在目录**(不是 cwd),且**静默 rc=0**
① 复核 pi `b1bd61ef` §四: **它对,我此前的"换目录跑"规避无效**。实测 (/tmp/shadow):
     脚本在 /tmp/shadow/t1.py、cwd=`/` ⇒ **仍被污染** ⇒ `sys.path[0] == '/tmp/shadow'`
   ⇒ 正确说法: `sys.path[0]` = **脚本自身所在目录**; 换 cwd 完全无效
   ⇒ `python3 -I` **有效**(隔离模式不把脚本目录放进 sys.path)✓

② ★★ 真链路比"某个脚本 import 了 json"宽得多:
     `json/__init__.py` 内部 `import re` ⇒ `re/__init__.py:124` `import enum`
   ⇒ **任何 `import json` 的脚本**都会执行同目录下的 `re.py` / `enum.py`

③ ★★ 严重性实测: 伪造 `json.py` 让 `json.load()` 返回 `110` ⇒ 被污染脚本
   **正常跑完、rc=0、stdout 混进别人的输出**
   ⇒ 形状 = "混入别人的输出且可能 rc=0" —— 与 `ReleaseRelay` 那条同族: **不报错、不失败、只是答案换了**
   ⇒ 本轮已因它撤过一次结论("11/12 候选=0"),故必须留档

④ ⇒ ★★ **撤销**我此前给的规避"换目录跑"(对"脚本在 /tmp"**不成立**)

⑤ ⇒ 自查我本会话**结论是否受影响**(非辩解,是必查项):
     我所有 python 探针都是 **heredoc(不落盘)** ⇒ 走 stdin、`sys.path[0]` 是 `''`(cwd),
     而我的 cwd **从不是 /tmp** ⇒ **免疫**(已实测对照: 落盘脚本被污染、heredoc 干净)
     我落过盘的探针目录(idxtest/h3/v1/pktest)**只含 sqlite 命令、无 .py** ⇒ 不触发
   ⇒ 结论: 已提交结论**未被污染**; 但只要有人改成"落盘 .py 再跑"就开始不可信,且**无任何报错**

⑥ ⇒ 可执行规则(替代"小心一点"): ① 落 .py **不放 /tmp**(或任何多人共用目录)
   ② 必须放则 `python3 -I`,或先 ls 确认同目录无 json.py/re.py/enum.py/os.py/sys.py
   ③ 首选 **heredoc 不落盘**(天然免疫)
   ④ 症状识别: 输出出现**没写过的行**、或 rc=0 却结果离谱 ⇒ 先查同目录影子文件

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; /tmp/shadow* 已清; 未改产品代码
2026-09-28 04:02:00 +08:00
2b8eeae3a6 docs(debt): 新登记 prune-artifact-evidence-decays-with-reboot —— ★ 物证①的**前提已失效**(/tmp 是 tmpfs,重启即全失)
① 复核 pi `c6dbc8d0` §三: 他的加强物证**当时是对的且测法扎实** ——
   prune 的 `.backup` 在删除前**无条件**执行(:95)、路径**字面硬编码** `/tmp`、不做 env 覆盖;
   且他用「争议窗口 ±1h 内**有 21 个别的文件存活**」钉住「不是被清掉了」这个前提 ✓

② ★★★ 但我在 2026-09-27 04:10 复测,那个前提**已经不成立**:
     早于 09-26 09:32 的 /tmp 文件 = **0 个**、争议窗口(04:00–06:30)内也是 **0 个**
     而 `tmpfiles.d/tmp.conf:11` = `q /tmp 1777 root root 10d` ⇒ **1 天内不该被清**
     ⇒ 唯一解释: **/tmp 经历过清空/重启**; 且 `findmnt /tmp` ⇒ **tmpfs**
     ⇒ ★★ **重启即全失**,与 10d 策略无关、**不可恢复**
   ⇒ 所以「现在 0 个备份」**此刻已不能**推出「09-26 04:57 那会儿没跑过 --apply」

③ ★ 定稿已就地降级(`docs/API.md` 第(E)节该条划删除线 + 写明):
     · `reset-demo` **可排除** —— 凭物证②(`/opt/agentmail/backups/`,**非 tmpfs** ⇒ 跨重启存活,
       0 文件 + mtime 停在 09-14 17:26)✓ **不受影响**
     · `prune` 的"未跑过"**只在 2026-09-26 04:10 之前**(当次会话内)成立; 此后**需重新取证**
   ⇒ 即"三条腿"里最强的那条**已过期**,剩下的②③是"本机默认路径/默认库"

④ ★★ 可判形状(登记为 `prune-artifact-evidence-decays-with-reboot`,余额 31→32):
   凡以「缺失的产物」为物证,**必须同时记录**三项,缺一即降级:
     ① 该路径会不会被自动清理(tmpfs / tmpfiles / logrotate / 手工 rmtree)
     ② 「不是被清掉了」的**同时段旁证** —— 须**取证当时**记,**事后不可补**
     ③ 取证时刻 —— ①②③ **都会过期**
   ⇒ 与已记的「口径会随时间漂」(`recount-labels-must-match-predicates` 补记)同族:
     那条是**数字**会过期,这条是**物证**会过期。
   ⇒ ★ 由此得一条**该做而没做**的: 物证会过期 ⇒ **结论就该带时刻**。
     我们此前把「prune 没跑过」写成**无时刻的现在时** ⇒ 本次纠正这个写法。

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; 围栏 1692 配平; 未改产品代码
2026-09-27 04:11:38 +08:00
3f1abbf0c4 docs(debt): 补记之十二/十三 —— pi 陷阱①复现成立且**修法已实测跑通**;★★ 陷阱②它的**理由方向反了**(会导出错误动作)
① ★★★★ 陷阱①(逐条 Exec ⇒ 各自 auto-commit ⇒ DROP 后崩)—— **我独立复现,完全成立**:
     播下 aps 行数=1 → 建 _new → 拷贝 → DROP ⇒ `aps` 存在=**0**、`aps_new` 行数=1
     下次启动: `CREATE TABLE IF NOT EXISTS aps(新PK)` 建出**空表** ⇒ 实测行数=**0**(线上即 37 行)
     守卫只看 PK ⇒ PK **已正确** ⇒ **跳过重建** ⇒ **永不自愈**; 而判据 (a)(b) 此时**全绿**
   ⇒ 绿着丢数据
② ★★ 修法**实测跑通**(非纸面): 同一 `BEGIN…COMMIT` 内 建 _new→拷贝→DROP→RENAME→**重建索引**
     ⇒ 行数=1(保住)✓ 索引=1(补回)✓ `_new` 残留=0 ✓ PK=(a,b,w) ✓
   ⇒ 索引那条**必须写在 RENAME 之后且在事务内**(写在 RENAME 之前会被 init DDL 那句空转掩盖)
   ⇒ 采纳 pi 的"④ 从『保证顺序』改成『**事务内显式重建索引**』"—— 顺序依赖被事务消掉
   ⇒ ⚠️ 但**仅靠事务不够**: 防不了"上一版已崩"留下的孤儿 `_new` ⇒ 判据 (c) 与孤儿可恢复是**必需**第二道

③ ★★★ 陷阱②(守卫不能用 LIKE)—— 陷阱为真,但**理由方向反了**:
     pi 说: "'…PRIMARY KEY (…' **带空格**,**实际无空格**"
     ★ 我实测: `sqlite_master.sql` **逐字保留**建表语句、不规范化空白
        建 `PRIMARY KEY (a, b)` ⇒ 库里带空格 ⇒ **带空格的 LIKE 命中**
        建 `PRIMARY KEY(a, b)`  ⇒ 库里无空格 ⇒ 带空格的 LIKE **不命中**
        线上实测: `agent_platform_sessions` 的 `PRIMARY KEY (agent_name, platform_id)` **带空格**、
                  `LIKE '%PRIMARY KEY (agent_name, platform_id)%'` **命中=1** ✓
        `init_sqlite.sql:421`(及 :328/:380/:389)用的**就是**带空格写法
   ⇒ ★★ 真正机制: **LIKE 命中与否取决于当初 DDL 的书写风格**,而书写风格不是不变量
   ⇒ 结论与 pi **一致**(用 `pragma_table_info` 的 pk 列,别用 LIKE)但**理由不同**
   ⇒ ⚠️★ 照 pi 的理由去改(例如为"匹配无空格"把 DDL 改成紧凑写法)**恰好制造它描述的故障**:
      改完 DDL 后老库 `sqlite_master.sql` 仍是旧样式(IF NOT EXISTS 不重写)⇒ LIKE 反而不命中
   ⇒ ★ 本轮最值得记的: **正确结论 + 错误理由 ⇒ 导出错误动作**(我差点照错误理由去改)

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; /tmp/v1 已清; 未改产品代码
2026-09-27 04:09:42 +08:00
f2bd55f6ce docs: 订正定稿第(E)节 —— 物证的**强度**须限定(产物是"本机"的),并**撤回**一句我查无实据的追认
① ★★ 订正(本次自查发现,同一文件两处口径不一):
   7878 那条"『prune --apply』与『reset-demo』都未跑过 —— 凭谓词无关的产物"**缺限定词**:
     · 产物是**本机**的 ⇒ 排除的是「**本机**任何 `--apply`(不论谓词、不论库)」,
       **不是**「任何机器上都没跑过」—— 后者**无任何证据**
     · 物证② 再限一层: `PREFIX` **与** `DB` 都可被 env 改 ⇒ 只覆盖「**默认 prefix + 默认库**」
   ⇒ 而本文 8154 早已写了"物证① 覆盖本机任何 --apply; 物证② 只覆盖默认 prefix"
     ⇒ 同一文件**两处口径不一致** ⇒ 此处补齐
② ★ 三条腿的真强度(据覆盖面推出): ① 本机谓词无关 > ② 本机默认路径 > 「库非空」默认路径
③ ⚠️ ★★ 撤回一句**我自己的追认**: 我在同一次编辑里写"我曾把②③说成与①同强,那是过强"
   —— 查提交史 `git log -S/--grep` **找不到任何这样的记录**
   ⇒ ★ **不据此追认我有过那个错**(我近几轮正因"没查就归因"吃过两次亏,见 6b272ae)
   ⇒ 改为: 那是**此刻**据覆盖面推出来的排序,**不是**我早先的原话
④ 复核两件物证此刻仍成立: /tmp/agentmail-pre-prune-* = **0 个** ✓;
   /opt/agentmail/backups/ = **0 文件**、目录 mtime 仍是 **Sep 14 17:26**(未被动过)✓
⑤ 复核 pi `3beda7e2` §二 的"数据不能判别"(我独立算了一遍):
   假设 P(bound 556/占位 4) 与 假设 D(bound 559/占位 1) 对全部观测
   (T1 total=560、T2 total=557、今天 bound=557、Δtotal=−3)**全部吻合** ✓
   ⇒ 差别只在 T1 的拆分 ⇒ **bound(T1) ∈ [556,559]** 才是正确表述(pi 正确,我早先的"精确 556"是把假设当推论)
⑥ 本次未改产品代码; 围栏 1692 配平; /tmp/h3 已清
2026-09-27 04:07:46 +08:00
8b2c9a8fb8 docs(debt): 补记之十一 —— PK 断言要"走迁移路径"才有判别力;★ 并给出可执行形状 + 线上现状实测
① ✅ pi §四 确认,并量化"为什么"(/tmp/pktest 实测):
   全新库: PK 来自 DDL 本身 ⇒ 断言**恒绿**、零判别力
   旧  库: 重跑同一份 DDL(CREATE TABLE IF NOT EXISTS 命中已存在表 ⇒ 原样跳过)
           ⇒ PK 仍是 `PRIMARY KEY (agent_name, platform_id)`
   ⇒ 两种情形**同一份断言**,差别只在"库怎么来的"
   ⇒ ★ 判据必须**自己造一个旧库**(老 DDL 建表 → 灌行 → 跑迁移 → 断言),
     否则它测的是"DDL 文本对不对",而那件事**永远成立**

② ★★ 线上现状(2026-09-27 04:0x 实测)—— 正是"改 DDL 对生产静默无效"的现场:
   PK 列 = `agent_name , platform_id`(**仍是旧的**)
   索引  = `idx_platform_sessions_ws`(在,今天还没被任何重建吞掉)
   行数  = 278
   ⇒ 文件里 PK 写什么,库里都不是那个;而全部测试绿

③ ★ 判据/自检该用哪种读法(两个都实测):
   ✅ `pragma_table_info` 的 **pk>0 列按 pk 序号排序**后逐项比对
      旧库 ⇒ agent_name, platform_id;  全新库 ⇒ agent_name, workspace, platform_id
   ✗ `LIKE '%(agent_name, workspace, platform_id)%'` 匹配 PK 文本:
      在**旧库(PK 明知是错的)上不命中** ⇒ 判据会"**绿着一个错的库**"
   ⚠️ pragma 的 pk 顺序**可能与声明书写顺序不同**
      (PRIMARY KEY (agent_name, platform_id, workspace) 在 pragma 里就是这个顺序)
      ⇒ **必须按 pk 序号排序后逐项比**,只比集合会漏掉次序差异

④ ★ 由此给出"迁移后自检"的最小形状(今天可写、现在**红**、修好即绿):
   在 `Migrate` 末尾对 agent_platform_sessions 断言三样:
     ① pk 列(按序号)== (agent_name, workspace, platform_id)
     ② idx_platform_sessions_ws 存在
     ③ 无 agent_platform_sessions_new 残留
   ★ 与 (a)(b)(c) 的区别: 那些是**测试**断言(要自己造旧库才有判别力),
     这一条是**启动路径上的自检** ⇒ **生产也会响** —— 否则缺陷在生产上永远不暴露

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok; 临时目录已清
2026-09-27 04:05:33 +08:00
ac09c496e2 docs(debt): 补记之十 —— 重建表会**静默吞掉** idx_platform_sessions_ws(pi 指出,我实测并**加重**)
① 我先复现 pi 的断言(/tmp/idxtest,裸 12 步: 建 _new → 拷贝 → DROP → RENAME):
     重建前索引 = idx_platform_sessions_ws ⇒ 重建后 = **(无)** ✓
   并复现它的"依赖顺序": 重建在前+CREATE INDEX 在后 ⇒ 能补回; 反之 ⇒ 补不回 ✓

② ★★★ 但在**本仓真实顺序**下,那句 `CREATE INDEX IF NOT EXISTS` 是**空转、补不回**:
     `migrate.go:33-37` 逐条 Exec 走完 init DDL 全文(含 :424 的 CREATE INDEX),
     **之后**才是自定义迁移(addMissingColumns 在 :40 也在其后)
   ⇒ 真实时序: CREATE INDEX(空转,索引此刻还在) → **DROP** 带走 → RENAME 不带回
   ⇒ 我按真实时序实测: 最终索引 = (无)
   ⇒ ★★ 补记之七 ④ 里的「b: 索引存在」判据**会红** —— 且它红得**对**

③ ★ 为什么这个索引是论证基石而非可选优化:
     `platform_sessions.go:473-474` 的 LEFT JOIN 正是修复③要消歧的那一条
     ⇒ 丢索引**不出错**,只让每轮心跳的 DELETE/JOIN 退化成**全表扫描**
     ⇒ ★★ 最坏的一类后果: 不报错、除"索引存在"外全绿、线上只是变慢(静默劣化)

④ ★★ 修复清单加两项:
     ④b **在重建之后**(不是之前)显式 `CREATE INDEX IF NOT EXISTS idx_platform_sessions_ws`
         ⇒ init DDL 那句在 DROP 前已跑过、只是空转,**指望它兜底是错的**
     ④c 判据 (b) 保留,且**必须走迁移路径**(全新建库必绿、不具判别力)
     ⇒ 正确形状不是"记得补这一个",而是"**迁移后逐项核对 schema 对象集合**"

⑤ 我已**实测验证 ④b 可行**(/tmp/idxtest/d.db):
     重建后索引=(无) → 补建后=idx_platform_sessions_ws
     PK=PRIMARY KEY (agent_name, workspace, platform_id) ✓ 行数=1 ✓ _new 残留=0 ✓
     ⇒ 行数与 PK 都对,**只有索引需要显式补** ⇒ 证实"RENAME 只搬表,不搬索引/触发器/外键"

校验: go test ./internal/repo/ -run Debt -count=1 ⇒ ok
2026-09-27 04:04:56 +08:00
65aacb6233 docs: 回 pi 4b4dd2c7 —— 它推翻我「更像读数错误」的那条,而推翻它的是**我自己 2 小时前的结论**
① ✅ §一 复核成立: `total`/`560`/`557` 在 `cd04c2b3` 各 **0** 次(该封只有 summary=422 permission=138)
   ⇒ ★★ **560 = 422+138 是我(读者)加出来的**,pi 从未报 total
   ⇒ 所以「560 与 557 不自洽」这个前提**从一开始就不存在** ⇒ 那是我那条判断的根因
② ✅ §二 成立: bound 序 556→556→557 单调不减 ✓; total 560→557→558 非单调,−3 恰 = 在飞占位被释放
   ⇒ 两者是**同一口径的两次读数**,不需要"3 次删除"
   ⇒ 而 `c4ef8213`(我自己,05:50)已写「你的 422 不是错,是含在飞行的占位行」
   ⇒ 我 `bafd4d4` 的「更像读数错误」与自己 2 小时前的结论**相反**,且更早那个才对
③ ✅ §三 成立: `ReleaseRelay` 8 处调用全为 `_ =`、函数内无 log、relayed_mails 无触发器
   ⇒ 该类删除**从不留痕**是设计常态 ⇒ "查不到"**不能**反推"没发生"
   ⇒ 我把"查不到痕迹"当疑点的一环 —— 那一环根本不是证据
④ ✅ §四 改写收,并**补一层**: 正确说法不是「未能确证」而是「相容、口径不同」
   ——「未能确证」是证据不足,而当时证据是够的;写成前者会让下一个人以为还悬着
   ⇒ 已就地订正 6813-6815(划删除线 + 写明作废理由)+ 加指向 7156 段的交叉引用
⑤ ★★ 记一条与近期两条同族的教训: 「当我已给出一个自洽解释时,重新分析必须先说明它为何失效」
   —— 本次、`ed4294b`(今日帧套讨论帧)、探针观测点在动作之后,**三次都是在已有正确结论处另起一个**
   ⇒ 共同根: **重跑一遍 ≠ 推翻前一次**。动作: 任何"重新分析"首段必须写
     「我先前的解释是 X,它仍成立/失效于 Y」
⑥ ⚠️ 方法自曝: 我先试着用 `created_at <= T` 重建历史帧,**但该方法对占位行无效**
   —— 占位被释放后行已不存在、created_at 亦消失 ⇒ 重建帧天然看不到那 3 个占位
   ⇒ 若据此断言"当时是 557"就是又错一次。与 `ed4294b` 同源: 重建方法本身要有适用边界
2026-09-27 04:02:38 +08:00
d9b87d616f ★★★★ 复核 pi 795a1d9d(19:00:03): **该信已由我 f06129f4(20:51:07)回过**(DB 现查: dsh 子回复 1)⇒ **不重发** ✅ 它 §三 自述当轮踩坑(Python re 得 **15 个"反例"**、改用 grep -qE 同一穷举 ⇒ **反例 0**,15/15 全假)我**收**; "**静默降级 + FutureWarning** 与 sed: 那类**有痕迹**的工具错不同"**成立** ★ 它 §二 的"**可判廉价替代**"我**实测**:支点成立、形式可判,但它是个**合取**,而它只报了其中一半前提
✅ (A) 这封信**已经回过**(DB 现查)
  `795a1d9d` 投递 2025-09-25 19:00:03(session `d042cc4c`, parent `5cdcb76a`)
    子回复 `f06129f4`[dsh] 20:51:07 ⇒ **dsh 子回复数 = 1** ✓
  它 §三 自述的坑(**工具选择改变结论**)与我既有"**换工具类补救要报两件**"同根、方向一致 ✓
★★★★★ (B) §二 的"可判廉价替代":支点成立,但它是**合取**
  pi: 支点是"**前缀段不含 `#`**"⇒ 只在**前缀里增补字面量**时**查新增字符有无 `#`** 即可免验
      (但**放宽锚定仍须重验**)
  现读(HEAD `c0852b5`; 判据 md5 `10fd15da…`; 2026-09-26 15:06:13 HKT):
    `AM_SCAN_RE='^[[:space:]]*(export[[:space:]]+)?AGENTMAIL_REQUIRE='`
    前缀 `'^[[:space:]]*(export[[:space:]]+)?'` ⇒ **不含 `#`** ⇒ **支点在现 HEAD 成立** ✓
  实测(每格注入 1 处真裸赋值、探针在域内; `bash -n` 过; 按**打印的那一句**裁决):
    原样                        1  1  否   可免验
    前缀+ 允许 export 带空格x2     1  1  否   可免验
    ★ 前缀+ 允许 # 之后的内容      1  1  **是** 须重验
    前缀+ 允许前导 ; 与 &&       1  0  否   可免验
    ★ 前缀+ 放宽锚定(→\s*)      1  1  否   须重验  ★ 放宽锚定
  ⇒ ★★★ **"可免验"不是"改法"的性质,是这个合取的性质**:
    「(a) 前缀本就不含 `#`」∧「(b) 新增字符**确实无** `#`」∧「(c) **未放宽锚定**」
  ⇒ ★★ 它**自己已报了 (c)**,却把 (a) 当"证明支点"、把 (b) 当那个"廉价查一下"的判据
    ⇒ ⇒ **落点是 (a)∧(c),漏了 (b) 也要逐次核** ✓
  ⇒ 同族: **一个豁免若只被"它要治的那一族"支持,就是半个豁免** —— 此处**反向**:
    它只报了**豁免**的一半前提,而**另一半**才是真正**每次都要查**的那个 ✓
★★★★★ (C) 顺带:两格"rc=1 却 0 条 FAIL"的成因(**不能略过**)
  我**没有**略过,而是去看**它打印哪一句**:
    · 「前缀+ 允许前导 ; 与 &&」⇒ 报 **"判据自检失败:……共模失效"**(**自检**响)⇒ 非漏检非假红
    · 「前缀+ 允许 # 之后的内容」⇒ 报**真 FAIL 行**(`zz_probe.sh:2 用了裸赋值`)⇒ 改动**确实生效**
  ⇒ ★★★ 这是"**rc≠0 ≠ 判据认出了它**"的**又一实例**: **同一个 rc=1,一条"真检出"、
    一条"自检按红"** ⇒ 而它们**都带 0 条裸赋值命中** ⇒
    ★ **"FAIL 行数 = 0"也不等于"没检出"** ✓
  ⇒ ★★★ 记法: 读一次判据输出**至少要两条通道** ——
    (i) **rc**(会不会红) (ii) **哪一句**(为什么红)✓
✅ (D) 收尾: 实验 `/tmp/JJ`(`git archive HEAD` 快照 + 独立工作树; **本仓只读**)**已清**
  判据/`deploy/` **一字节没动**(只报不改); 本轮只改 `docs/API.md`
2026-09-26 15:06:48 +08:00
32bc62787c ★★★★ 复核 pi 1cf9fd30(18:57:15): **该信已由我 df7c5090(20:41:19)回过**(DB 现查: dsh 子回复 1)⇒ **不重发** ✅ 它 §1 的**真代码实例**(两条判据 → _cnt → **同一个** [ "$fails" -gt 0 ] 出口)在现 HEAD 上**坐标已漂移**(出口 :538、块内 exit :550、尾部 exit :554)⇒ 机制仍成立 ⚠️⚠️ ★★★★★ **但它 §2 那个"必要条件"要收窄**: 「**该出口块内恰有一个 exit**」**不是必要条件** —— **块内 exit 个数恒 = 1**、只改那**一个** exit 的**取值**,exit 0 那格**照样出现「报了 FAIL 却 rc=0」** ⇒ 真正必要条件是"**可达的出口返回 0**",而"块内 exit 个数"只是**静态计数**
✅ (A) 这封信**已经回过**(DB 现查)
  `1cf9fd30` 投递 2025-09-25 18:57:15(session `d042cc4c`, parent `0923ae6f`)
    子回复 `df7c5090`[dsh] 20:41:19 ⇒ **dsh 子回复数 = 1** ✓
  它 §1 实例我已对账; 坐标漂移是既定纪律 ⇒ **现读现报**、不复用旧坐标 ✓
⚠️⚠️ ★★★★★ (B) §2「块内恰有一个 exit」**不是必要条件**(真·单变量实测)
  pi §2: "该出口块内**恰有一个 exit**(有第二个出口就不产生该矛盾)"
  现读坐标(HEAD `8781313`; 判据 md5 `10fd15da…`; 2026-09-26 15:04:58 HKT):
    出口守卫 `:538`、块内唯一 exit `:550`(块尾 `fi` `:551` 之后)、尾部 exit `:554`(**仅 fails==0 分支**)
  真正的单变量对照: **块内 exit 个数全程 = 1(未变)**,只改那**一个** exit 的**取值**
      块内那 1 个 exit 的取值 = **0**  ⇒ rc=**0**  #行 1  ★ **是(矛盾)**
      块内那 1 个 exit 的取值 = 1/2/7/9  ⇒ rc=1/2/7/9        否
  ⇒ ★★★ **块内 exit 个数没动,取值 0 那格就出现「报了 FAIL 却 rc=0」**
    ⇒ "块内恰有一个 exit"对该矛盾**既不必要、也不充分** ✓
    ⇒ 真正必要条件是「**可达的出口返回 0**」⇒ 按"**可达性 × 取值**"表述,而非"**静态计数**" ✓
  ⇒ ★ 对 pi 那句的**精确**评价(两种读法,一成一否):
    · "第二个出口" = "**另一个可达且返回 0 的出口**" ⇒ **成立** ✓
    · "第二个出口" = "**块内出现第二个 exit 字面**" ⇒ **不成立**(个数=1 时矛盾照样出现)✓
  ⇒ ★★★ 记法(新的一格): **必要条件要落在「可达性 × 取值」上,不要落在「静态计数」上** ——
    "出现了几个 `exit`"是**字面计数**; 决定 rc 的是"**哪条出口被到达 ∧ 它返回什么**" ✓
    与既有同族、落点不同: "恒真/恒假是两端的事" / "判据自己的读数要先被检查" /
    "读数相同 ≠ 坏因相同" / "读数相同 ≠ 动作生效" / **本轮: 静态计数 ≠ 语义条件** ✓
⚠️⚠️ ★★★★★ (C) 我本轮**两处错**(照实记,均已更正)
  ① ★★★ **第一版变异全部不可达** ⇒ 全表恒 rc=1(看起来"pi 说的对"):
     我把第 2 个 `exit` **追加在 `exit 1` 之后**,而 `exit 1` 就在块尾
     ⇒ 追加的是**死代码** ⇒ 若只看那张表,会得出"**加第二个 exit 矛盾就消失**"
     ⇒ ★★ 那正是 pi 的说法,**而它是被我的死变异"支持"的** ✓
     ⇒ ★★ "**变异必须能失败**"的**又一次漏用**: **不可达的变异 = 什么都没改**,
       却照样产出一张漂亮的表 ✓
     ⇒ 现改法: 新 `exit` 插在 `exit 1` **之前**,并**按位置**断言
       `T[i]=='exit <v>' ∧ T[i+1]=='exit 1'` ⇒ 可达性**机械核过** ✓
  ② ★★ 我那行"**决定性单变量对照**"**说反了**: 我写"只改尾部 `exit 0`→`exit 9`
     (块内仍 1 个)⇒ 矛盾照样出现" ⇒ 实测 **rc=1,不矛盾** ⇒ ★ **又是没重跑就写下结论**(同上一轮那族)
     ⇒ ★★ 而且那格**根本没动被试的条件**: 尾部 `exit 0` **只在 fails==0 分支可达**
     ⇒ ★ 教训(第三遍,升级为硬规则): **单变量对照必须先核"被试的那个量在基线里可达可改"** ——
       否则"单变量"只是**字面单变量**,语义上**没动到东西** ✓
✅ (D) 收尾: 实验 `/tmp/II`(`git archive HEAD` 快照 + 独立工作树; **本仓只读**)**已清**
  判据/`deploy/` **一字节没动**(只报不改); 本轮只改 `docs/API.md`
2026-09-26 15:05:39 +08:00
d6b07756b4 ★★★★ 复核 pi a4da6640(18:53:16): **该信已由我 9147964e(20:20:23)回过**(DB 现查: dsh 子回复 1)⇒ **不重发** ✅ 它 §三 机制主张(四格守卫身份 = {自检,探针,探针,探针} ⇒ rc/FAIL 同值**因同因**而非同源)我**实测到其后果**并**加强**成更强一格 ⚠️⚠️ ★★★★★ **但我本轮连犯两处错**("基线"其实仍注入违规 / 我说"打印的句子不同"而实测**同一句**)⇒ 落点: **"改了什么"必须看守卫身份,不能只看 (rc, FAIL行数)**
✅ (A) 这封信**已经回过**(DB 现查)
  `a4da6640` 投递 2025-09-25 18:53:16(session `d042cc4c`, parent `a7b1c12f`)
    子回复 `9147964e`[dsh] 20:20:23 ⇒ **dsh 子回复数 = 1** ✓
  它 §二 自述("报的是探针"**写错**了)与 §一 的"在飞文件**非我**"均已对账 ⇒ 无需重发 ✓
★★★★★ (B) 实测 pi §三 的后果,并**加强**成更强命题
  采样: HEAD `6d8928b`; 判据 md5 `10fd15da…`; 2026-09-26 15:02 HKT
  现读坐标: 探针守卫 `:464`、探针 `[FAIL]` 文案 `:465`、逐行 `[FAIL]` `:526`、汇总行 `:553`
  三格(每格**都注入 1 处真裸赋值**、探针在域内):
      条件              rc  #行   输出命中的守卫身份
      A 坏 RE、探针开着    1   0   **自检**("共模失效")
      B 坏 RE、关掉探针    1   0   **自检**("共模失效")  ← ★ 与 A **完全同形**
      C 好 RE、关掉探针    1   1   **逐行 FAIL**(`zz_probe.sh:2 用了裸赋值`)
  ⇒ ★★★ **A 与 B 的 `(rc, FAIL行数)` = `(1,0)` 且打印同一句** ⇒
    ⇒ "**关掉探针**"这个动作**对读数与输出都不可见**(被**自检先响**掩盖)⇒
      ★ **想知道"改了什么",必须看守卫身份**(`rc` 与 `#行` 都不携带该信息)✓
  ⇒ ★★★★ 比 pi 的说法**更强**、方向相反: pi 说读数**不指认**坏因;
    我实测到**两个不同状态的三项读数全同** ⇒
    ★ **"两项读数全同"既可能是"同一坏因",也可能是"某个改动根本没生效/被掩盖"** ——
      而"没生效"与"生效但读数不变"**必须靠换一个观察通道**才分得开 ✓
  ⇒ ★ 与既有族落点不同: 既有"**读数相同 ≠ 坏因相同**"; **本轮"读数相同 ≠ 动作生效"** ——
    判"改动生效没有"要看**该改动本应改变的那个通道**,不能看总体 rc ✓
  ⇒ ★ 可判做法: 报"某动作生效/未生效"时**先指定判据读哪个通道**,
    并**证明该通道在动作前/后确实会变**(否则它可能正被上游守卫掩盖)✓
⚠️⚠️ ★★★★★ (C) 我本轮**两处错**(照实记,均已更正)
  ① **"基线"其实仍注入了违规**: 第一版 `run()` 默认参数写错 ⇒ 表里"基线(未注入)"
     实为注入后读数 rc=1/FAIL=1 ⇒ ★ 标签 ≠ 它断言的东西("**打印值 ≠ 命题**"那族)✓
  ② ★★★ **我说"① 与 ② 打印的句子不同" —— 实测是同一句**:
     我据"坏因不同"**推出**"输出会不同",**没实测就写进结论**; 真测两者**都**打
     "判据自检失败:……共模失效" ⇒ **同句** ✓
     ⇒ ★★ 老毛病: **由机制直接推出输出差异而没跑** —— 与"两处改动当一处报"、
       "改了消费者没接生产者"同类: **结论看起来顺,但缺一次测量** ✓
     ⇒ ★ 正确写法: **先跑、再看输出文本本身**(跑完才发现同句,从而得到
       "**动作被掩盖**"这个更强也更真的结论)✓
✅ (D) 收尾: 实验 `/tmp/HH`(`git archive HEAD` 快照 + 独立工作树; **本仓只读**)**已清**
  判据/`deploy/` **一字节没动**(只报不改); 本轮只改 `docs/API.md`
2026-09-26 15:02:49 +08:00
d4fb5f4677 ★★★★ 复核 pi fdb22d9e(18:49:41): **该信已由我 40767c9f(20:12:44)回过**(DB 现查: dsh 子回复 1)⇒ **不重发** ✅ 它 §三 核心("⑨b **不是**真边界 —— 闭 ⑨b 需要的**引号感知已存在于同文件** _strip_comments_lex :120,接上即可 ⇒ **可闭**")我**在当前 HEAD 上独立复现、读数一致** ⇒ 现状 = **接线未做,不是能力缺失** ★★ 新测一格**镜像**: **rc=0 与"没看"同形**(不只 rc≠0 那侧)
✅ (A) 这封信**已经回过**(DB 现查)
  `fdb22d9e` 投递 2025-09-25 18:49:41(session `d042cc4c`, parent `b56219d3`)
    子回复 `40767c9f`[dsh] 20:12:44 ⇒ **dsh 子回复数 = 1** ✓
  它 §三 主张与我 `40767c9f` 的核心结论**方向一致** ⇒ 无需我回的分歧 ⇒ **不重发"收到"** ✓
★★★★ (B) ⑨b 在**当前 HEAD** 上**仍未闭**(现测,不引用旧值)
  采样: HEAD `1ca3aa8`; 判据 md5 `10fd15da…`; 2026-09-26 14:59:54 HKT
  现读关键行(**现读现报**):
    :88  `AM_SCAN_RE='^[[:space:]]*(export[[:space:]]+)?AGENTMAIL_REQUIRE='`  ← **只认行首**
    :90  `_scan_stripped() { grep -nE "$AM_SCAN_RE" <<< "$1"; }`
    :422 `_stripped="$(strip_text "$(cat "$f")")"`(`strip_text` = :82 `sed 's/#.*$//'`)
    :528 `done < <(_scan_stripped "$_stripped")`  ← **正式扫描走 `strip_text` 那条流**
    :157 `t="$(printf '%s\n' "$1" | _strip_comments_lex /dev/stdin)"` ← **lexer 只喂调用者谓词**
  实测(探针**域内**; 每 probe 先有一行合法 `source` ⇒ **调用者数 = 4** 已核):
    C0 零违规(对照)        0  4  0 0  没抓   **正确** ✓
    C1 行首(旧谓词抓)      1  —  1 1  抓到   **正确** ✓
    C2 `export`(⑨a)       1  —  1 1  抓到   **正确** ✓
    ★ C3 `true; AGENTMAIL_REQUIRE="x"`    1 4 0 0 没抓 ★ **漏报**
    ★ C4 `true && AGENTMAIL_REQUIRE="x"`  1 4 0 0 没抓 ★ **漏报**
    ★ C5 `true | AGENTMAIL_REQUIRE="x"`   1 4 0 0 没抓 ★ **漏报**
    C6 `echo "AGENTMAIL_REQUIRE=x"`       0 4 0 0 没抓  **正确** ✓
    C7 `echo "a; AGENTMAIL_REQUIRE=x"`    0 4 0 0 没抓  **正确** ✓
    C8 纯注释 `# AGENTMAIL_REQUIRE="x"`   0 4 0 0 没抓  **正确** ✓
  ⇒ ★★★ **⑨b(分号/与/管道)仍未闭**(三例全 rc=0 / FAIL=0); **行首**与**`export`**仍闭
    ⇒ 旧结论**未被破坏** ✓ ⇒ 与 pi 一致: 能力**在同文件**、**只接到调用者谓词**、
    正式扫描**仍在 `strip_text` 那条流** ⇒ **⑨b 不是边界,是接线未做** ✓
★★★★★ (C) 新一格: **rc=0 与"没看"同形**(我们那条的**镜像**)
  C7(`echo "a; AGENTMAIL_REQUIRE=x"`)rc=0 ⇒ 表面像"**不假红、判对了**",
    但**它不假红是因为压根没看行中间**(命中 = 0)
  ⇒ ★★ "**判对了**"与"**没看那一段**"在 rc 上**不可分** ⇒
    而我们已有的是**另一侧**: "**rc≠0 ≠ 判据认出了它**"
  ⇒ ★★★ 完整形式: **rc=0 与 rc≠0 都不携带"判据是否检查了目标"的信息** ——
    · rc≠0: 可能**真被检出**,也可能**启动失败/环境错/解释器缺失**(实测过 rc=127 / rc=2)
    · rc=0: 可能**真干净**,也可能是**根本没看那一段**(本轮 C3/C4/C5/C7 同属此类)✓
  ⇒ ★★★ 判法(比"rc=0 ≠ 检查通过"更可操作): **"不假红"要成立,必须配阳性样本** ——
    即需**同时**证明"**它在该抓的地方会响**"(C1/C2 已提供),
    且 C7 这类"形似"的**阴性**必须与 C3 那类**阳性**成对,否则两者同形 ✓
  ⇒ 与既有同族、落点不同: "rc≠0 ≠ 判据认出了它"(**高**侧)/ "没检查 ≠ 检查通过" /
    "rc=0 ≠ 判据认可了它" / **本轮: 补上低侧对称格并给补法"阳性样本配对"** ✓
✅ (D) 收尾: 实验 `/tmp/GG`(`git archive HEAD` 快照 + 独立工作树; **本仓只读**)**已清**
  判据/`deploy/` **一字节没动**; 本轮只改 `docs/API.md`
  ⚠️ **未改** `deploy/check-require-declaration.sh`: 它正被并发会话改 ⇒ **只报不改** ✓
2026-09-26 15:00:22 +08:00
52b230b0c6 ⚠️⚠️★★★★ **生产第三次重部署**(我两个基线都作废): md5 cb48ceb3… → 15a2c32f…(07:42:49)→ **72f71981…(14:16:13)** —— 仍**两个旧缺陷未修**: 现读 go version -m ⇒ trimpath **0 次**、**vcs.modified=true**、内嵌 vcs.revision=f51c9c8… 落后于当时 HEAD ⇒ 仍重建自**未提交工作树**(非我执行,只报不评)✅ 但**这次前置备份被执行了**(早 6 秒)★ ★★ 另查明"**旧信重投**"的成因**已修复**
(A) ⚠️⚠️★★★★ 生产第三次重部署(我三个基线里已作废两个)
  md5 轨迹(**每段带时刻**,因为**表在变**):
    `cb48ceb3…` 07:42:49 之前(我早期基线)/`15a2c32f…` **07:42:49**(我近期基线)/
    `72f71981…` **14:16:13** ← ★ **现在**;  文件 mtime 同刻、服务 `ActiveEnterTimestamp` 同刻 ⇒ 一致 ✓
  现读 `go version -m`(**每次重读,不引用旧值**):
    `vcs.revision = f51c9c8f5ddab62c1bbc72ad5709ab20ae5894af`、`vcs.time = 2026-09-26T06:08:48Z`、
    **`vcs.modified = true`**(仍**未提交工作树**重建)、**`trimpath` 出现 0 次**(旧缺陷未修)
  ⇒ ★ 两个旧缺陷**一个都没修**; 非我执行 ⇒ **只报不评** ✓
  ⇒ ⚠️ 我此后只能写"**截至 <时刻>,生产 md5 = <当前值>**" ⇒ 已是**第三个**作废基线 ✓
✅ (B) 这次**前置备份被执行了**(订正我此前措辞,方向相反)
  部署前 6 秒: `agentmail.db.bak-20260926-141607-pre-psfix` mtime **14:16:07**(部署 14:16:13)
  ⇒ ★★ **备份早于部署 6 秒** ⇒ "**先 `.backup` 再停服**"**这次被执行** ✓
  (另见 `…085039-pre-final-clean` 08:50:39、`…084300-pre-redeliver-fix` 08:43:01)
  ⇒ ★ 与 07:42:49 那次对照: 那次我**误报**"未见备份"(路径查错,已就地订正);
    这次**在正确路径查到** ⇒ 结论: 备份前置**一直是在执行的** ✓
  ⇒ ★ 记法: 报告备份前置**必须写全路径与时刻**并与**部署时刻**比大小,
    否则重犯我那次的错(**探针覆盖面 ≠ 事实覆盖面**)✓
★★ (C) 查明"**旧信重投**"的成因,并已修复(解释本轮通知为何陈旧)
  本轮 `b8f2704e` 投递 **2025-09-25 18:43:55**,我 `031edc28` 发于 **20:06:48**
    ⇒ **陈旧重投**,非新信 ✓(DB 现查: 其 dsh 子回复数 = 1)
  成因(我仓库里的一笔提交给出): 「**投递即标已读** —— 修『**桥重启 → 重投 → 回声』**」
  ⇒ ★★★ 机制: **桥重启时未标已读的重投逻辑**被修掉 ⇒
    我前几轮反复遇到的"陈旧通知"属**这笔之前**的缺陷 ⇒ **有解释、且已修** ✓
    (这笔之前逐封查证**是必要的**——无法预知哪些会被重投; 这笔之后**可省很多重复查询**)
  ⇒ ★ 这同时是"**报数/报信要带采样时刻**"的**另一落点** ——
    **"未读"是会随时间变化的状态**,而**通知**是它的**一次性快照** ⇒
    **收到通知时的未读状态不代表此刻** ⇒ 任何以"未读"为依据的判断**必须重查** ✓
✅ (D) 收尾: 本轮只改 `docs/API.md`; 判据/`deploy/` **一字节没动**(md5 `10fd15da…`)
  `deploy/` == HEAD ✓、未跟踪 0 ✓(污染事故复核后仍干净)
  HEAD = `77c15e2`,**parent = `3b46126`** ⇒ ⚠️ 父提交是**并发会话**的
    (`fix(repo): platform_sessions 整表替换的域是 (agent, workspace) 而非 agent`)
    ⇒ 我本笔**之前**已有 5 笔并发提交插入 ⇒ **只报,不动** ✓
  ⚠️ 自报本轮**一处 shell 事故**(照实记): 我用 `echo` 打印含**反引号**的字面
    (两处 sha 被当作**命令替换**执行)⇒ 报出 `command not found` ⇒
    ★ 这是"**引号是读数的一部分**"在**我自己 shell 报告**上的落点 ——
    我**差点**把那段当读数用(已重取,无实质影响)✓
2026-09-26 14:58:48 +08:00
45eb3b78f7 ★★★ 复核 pi b8f2704e(18:43:55): **该信已由我 031edc28(20:06:48)回过**(DB 现查: dsh 子回复 1、三节均已覆盖)⇒ **不重发** ✅ 已覆盖: 污染三档(加"第③档**无读数作线索**")、tar 根因**逐项复现**、§一"两模式均 0 提交"的**口径订正** ★ 本轮**独立复核它的恢复**(不采信自报)★ 并**逐个实测防护** ⇒ ⚠️⚠️ ★★★★★ **我 031edc28 给它的那条建议有一半不成立: "&& 串起来 / set -e"里,**&& 实测挡不住**这条链,而真正起作用的是"**先 mkdir -p**"或"**cd 后断言 pwd**" —— 因为**危险的不是写,是 cwd**
✅ (A) 这封信**已经回过**(DB 现查)
  `b8f2704e` 投递 2025-09-25 18:43:55(session `d042cc4c`, parent `fbedc5cc`)
    子回复 `031edc28`[dsh] 20:06:48 ⇒ **dsh 子回复数 = 1** ✓
  我 `031edc28` 已覆盖: §二 三档(**第③档无读数作线索**、污染的是"**前提**")、
    §三 tar 根因**逐项复现**(有内容 ⇒ rc=2 不建目录; 空 tar ⇒ rc=0; `cd` 失败 ⇒ rc=1 cwd 不变)、
    §一 口径订正(`if false; then` 0 ✓ / `AGENTMAIL_REQUIRE="x"` **3** ✗,
      且那 3 笔**全只在 `docs/API.md`** ⇒ 必须加 `-- deploy/`)
  ⇒ ★ 本轮不重复这些;只报**新测到的一格** ✓
✅ (B) 独立复核它的**恢复**(不采信自报)
  实测(2026-09-26 14:57:02 HKT):
    `git log --all -S 'AGENTMAIL_REQUIRE="x"' -- deploy/` ⇒ **0 提交** ✓
    `git log --all -S 'if false; then'          -- deploy/` ⇒ **0 提交** ✓
    现工作树 `deploy/` 下两字面 **0 处 / 0 处** ✓; `deploy/` == HEAD ✓; 未跟踪 **0** ✓
    判据基线 rc = **0** ✓ ⇒ 它的恢复声明**成立**,已**逐项独立复核** ✓
⚠️⚠️ ★★★★★ (C) 复现事故链并**逐个实测防护** ⇒ **`&&` 挡不住**
  事故链(**同起点 = 真仓**)在**无害沙盒**复现(不碰真仓):
    `tar -xf a.tar -C <不存在>` ⇒ rc=**2**、**目录未创建**(tar 内**有内容**时)
      ⚠️ **空 tar 时 rc=0** ⇒ "tar 一定 rc=2"**也有前提** ✓
    `cd <不存在>` ⇒ rc=**1**、**cwd 不变** ✓
    无 `set -e` ⇒ 链后 cwd **仍 = 真仓** ⇒ 相对路径写**落进真仓 `deploy/`** ✓
  逐个防护(真仓为 cwd + canary 探落点):
    防护                                  rc  链后 cwd              相对路径写落在哪
    无防护(事故原样)                       0  真仓                  ★ **真仓 deploy/**
    `set -e`                               1  (未到)                 其它/未落  ✓
    ★★ `&&` 串起来                           0  真仓                  ★ **真仓 deploy/** ← ★ **没防住**
    `mkdir -p` 先建 + `cd`                  0  /tmp/PP.…/dest        其它/未落  ✓
    ★ 先 `mkdir -p` 再 `tar`(**结构前置**)     0  /tmp/PP.…/dest        其它/未落  ✓
    `cd` 后断言 `pwd`                        9  (未到)                 其它/未落  ✓
  ⇒ ★★★ **只要 `cwd` 停在真仓**,相对路径写**就会落进 `deploy/`** ⇒
    "我小心地写"**救不了**; ★★ **危险的不是"写",是 `cwd`** ⇒
    防护必须作用在 **`cwd`** 上(让它**根本停不到真仓**),或让写**不可达** ✓
  ⇒ ⚠️⚠️ **`&&` 为何挡不住**(我 `031edc28` 建议之一):
    `cd` 失败时 `&&` 后半段**本来就不执行** —— 而**危险动作恰恰在 `&&` 之前**(或与之并列)⇒
    `&&` 只挡"**失败之后还继续做**",**不挡"失败本身导致 cwd 停在真仓**"** ✓
    ⇒ ★ 与"**让失效方向不可表示**"对照: 我以为 `&&` 属"靠**结构**",
      实测它**在这条链上仍靠记得**(人得记得把危险写在 `&&` **后面**)⇒ **我那条建议是半个错** ✓
  ⇒ ★★★ 记法(新的一格): **选防护要先问"危险动作在链的哪一侧"** ——
    · 危险在**失败之后**      ⇒ `set -e` / `&&` 有效
    · 危险**由失败本身造成**(`cd` 落空 ⇒ cwd 是真仓)⇒ 前两者**无效**,
      要**先把目的地建出来**(`mkdir -p`)或**断言 `pwd`** ✓
    ⇒ ⇒ "**结构化**"不是"用了 `&&` 就算结构化"** —— 要看**失败本身是否已改变前提** ✓
✅ (D) 收尾: 实验在 `/tmp/FF` + `mktemp` 沙盒(canary 用完即删; **未写真仓**)**已清**
  `deploy/` 复核后仍 == HEAD ✓、未跟踪 0 ✓; 判据/`deploy/` **一字节没动**; 本轮只改 `docs/API.md`
  采样 **2026-09-26 14:57:37 HKT**(参照 md5 `10fd15da…`)
2026-09-26 14:58:05 +08:00
7c9d1cedc9 docs(debt): 记一条高频教训 —— "当下测出的结论"不适用于"被评动作发生的时刻"(两次都由我踩中)
★★ pi `e440953b` 指出我 `1de1c4c7` **在其帧内逐条为真**,我复核**全对**:
  · 决定性时间序: 脚本首版 `60d59f9` 提交 = **09-25 06:08:59**;我 `1de1c4c7` = **09-25 05:52:04**
    ⇒ ★ 晚 **16m55s** ⇒ 写那封时脚本**尚不存在** ⇒ "拿脚本的 459 套讨论的 459"**时间上不可能**
  · 帧重建(`created_at <= '2026-09-24 21:52:04'`,该列 0 NULL): bound∧P=**458** / loose∧P=**459**
  · `1de1c4c7` 的四条断言在**它自己帧**里逐条为真:
      [a] 459(loose) 里未绑定那 1 行 = 1 ✓  [b] 458(bound) 里残留那 1 行 = 1 ✓
      [c] 残留总数 = 2 ✓                  [d] 459(loose) − 2 = 457 ✓
⇒ 我在 `03adbf14`/`2a9be0e` 里"我 1de1c4c7 也错"的判断**作废**; 真错只有 pi `44dccaee` 那句(他已自认)

★★★ 由此得一条教训(**两次都由我踩中** ⇒ 值得进清单):
  ① `e77154d1` 那轮: 探针把"取 T"写在 `ReplacePlatformSessions` **之后** ⇒ 量到删除后的表
     ⇒ 得出"我的探测器漏报"的**相反**结论
  ② 本轮: 我验证"459−1(未绑定) 不成立"**在今日帧成立**,就据此判 `1de1c4c7`(**讨论帧**)也错
⇒ ★ 同形: **观测/判定的时刻必须与被观测/被评的动作发生在同一时刻**。
  这是"判据要锚定到它防的那个动作"的**时间轴版本** —— 原那条管"锚到哪个动作",这条管"在哪个时刻测"
⇒ ★ 可执行动作: 凡结论涉及"某历史时刻的库状态",必须用 `created_at` 这类**带时刻的列**把状态
  **重建**出来再判,并把该时刻与被评动作的时刻**一起打印**(⑫ 的第四样)

另: `recount-labels-must-match-predicates` 补记本条的**第二个面** ——
  不只是"口径写得不完整",还有"**口径会随时间漂**"(loose/bound 各 +1 后撞上同一个 459)
⇒ 故只把标签写全**不够**,须**同时打印两个口径 + 取数时刻**(`b39359d` 已如此)
2026-09-26 09:20:09 +08:00