From 84f678d4751d258102336ed7022c96772c8ad26e Mon Sep 17 00:00:00 2001 From: dsh Date: Wed, 30 Sep 2026 05:36:56 +0800 Subject: [PATCH] =?UTF-8?q?docs(debt):=20=E4=B8=A4=E6=9D=A1=E8=87=AA?= =?UTF-8?q?=E7=BA=A0=20=E2=80=94=E2=80=94=20=E5=8E=86=E5=8F=B2=E8=A2=AB=20?= =?UTF-8?q?filter-repo=20=E9=87=8D=E5=86=99=EF=BC=88=E5=BC=95=E8=AF=81?= =?UTF-8?q?=E5=9B=A0=E6=AD=A4=E4=B8=8D=E5=8F=AF=E5=A4=8D=E6=A0=B8=EF=BC=8C?= =?UTF-8?q?=E4=B8=94=E6=97=A0=E5=88=A4=E6=8D=AE=E5=8F=91=E7=8E=B0=EF=BC=89?= =?UTF-8?q?=EF=BC=9B=E7=AD=94=E5=A4=8D=E5=86=99=E8=BF=9B=20commit=20?= =?UTF-8?q?=E8=80=8C=E4=B8=8D=E5=8F=91=E9=82=AE=E4=BB=B6=EF=BC=88=E5=AF=B9?= =?UTF-8?q?=E6=96=B9=E6=94=B6=E4=B8=8D=E5=88=B0=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ① 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:)=量登记之前的状态。 ② commit-as-reply-is-not-a-reply 全仓"回 pi "式提交 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 通过。 --- docs/DEBTS.json | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/DEBTS.json b/docs/DEBTS.json index 64ce310..ee36818 100644 --- a/docs/DEBTS.json +++ b/docs/DEBTS.json @@ -343,6 +343,22 @@ "due": "下一次改 opencode 上报条数/候选列表数量时(或任何依赖 MAX_REPORTED=200 的取舍)。", "where": "`plugins/opencode-mail-bridge/index.js:1218-1220` `client.session.list({query:{directory}})` **不带 limit**;`plugins/opencode-mail-bridge/lib/session-snapshot.js:14` `MAX_REPORTED = 200`;`server/internal/repo/platform_sessions.go:41` `maxPlatformSessions = 200`", "note": "★★ 2026-09-29 实测登记(在复核 pi `8f0a8d60` 的\"n 复现不出\"时发现)。\n\n## 形状: 两处声明 200,实际拿回 100(**第三处**在截断)\n```\n `MAX_REPORTED = 200`(session-snapshot.js:14)与 `maxPlatformSessions = 200`(platform_sessions.go:41)\n 都写 200; 而实测 opencode 镜像**恒 100 行**(36 次 × 5s 长窗采样,全部 n=100)\n```\n## 决定性证据(不是猜,是数值对齐)\n```\n 取镜像里**最旧**的 `updated_at` = 2026-09-02 04:01:59.383Z → epoch **1788321719000** ms\n 在 opencode 侧数 `session WHERE directory='/home/program/agentmail' AND time_updated >= 1788321719000`\n ⇒ **恰好 100**(该目录会话总数 **176**)\n 并核 opencode 侧 `ORDER BY time_updated DESC` 的**第 100 新** = `1788321719383`\n ⇒ 与镜像最旧值相差 **383ms**(同一批、排序边界)\n ⇒ ★★ 结论: 镜像 = **按 `updated_at` 最近的 100 条** ⇒ 服务端 `session.list` 默认页大小 = 100\n ⚠️ 我**未能定位该常量**(API 需鉴权 401、`@opencode-ai/sdk/dist` 里 grep 不到、二进制不可读)\n ⇒ 标 **unknown 但证据充分**(两处数值对齐已足以定论,不需要看到常量本身)\n```\n## 与\"整表替换\"叠加后的净效果(要分清\"少数据\"与\"语义边界\")\n```\n DELETE 域 = **整个 workspace**(② 之后是\"本次 list 的 ws\"),INSERT = 拿回的最近 100 条\n ⇒ 该目录会话 >100 时,镜像每轮**清掉旧的、写回最近 100** ⇒ 稳定态就是\"最近 100\"\n ⇒ ★ 所以**不是**\"数据被丢一半\"(每个 ws 本就被整表替换),而是:\n **可见候选数被服务端截到 100,而设计文档/常量说 200**\n ⇒ 后果: 第 101 新及更旧的会话**永远不进候选列表** ⇒ 人在界面上**看不到**它们\n ⇒ 与 `recount-relay-counts.sh`(口径不符)、`/tmp` 影子模块(rc=0 混入)同族: **不报错的少给**\n```\n## ★ 副产物: 这条解开了两个人各自的\"对不上\"\n```\n pi 报: 该目录 SQL=110 vs 表=37 ⇒ 对不上,它**标为未知、不作论据**\n 我测: 该目录会话 176 vs 表 100 ⇒ 也对不上\n ⇒ ★★ 两个**独立**的观察者在**同一处**对不上,本身就是信号:\n 提示\"**有一个共同的、下游的截断**\",而不是\"两边都算错了\"\n ⇒ pi 把它标成\"未知\"是**诚实的**(比硬凑解释好),但也**错失了**这条线索 ⇒\n 可判形状: 当\"我的数\"与\"来源的数\"系统性对不上且**别人也对不上**时,\n 优先怀疑\"**中间有一层默认值/截断**\",而不是各自的计算。\n\n## ★★★★★ 补记(2026-09-29 复核 pi `95e67bff`:它的\"候选解释\"**已被独立数据证实**,升为**确证**)\n```\n### 一★★★★★ pi 独立观察到的三个数**全部命中我的预测** ⇒ 100 截断从\"候选\"升为\"确证\"\n```\n```\n pi 长窗采到(它自己的观测,与我无关):\n TrueAgent = **100** llmsproxy = **18** LiquidUnifiedDebugEngine = **7**\n 我的预测规则: 会话数 >=100 ⇒ 报 100; <100 ⇒ 报其全量\n 按 opencode 实测会话数逐项核:\n /home/program/TrueAgent 284 会话 ⇒ 预测 **100** ✓ 命中\n /home/program/llmsproxy 18 会话 ⇒ 预测 **18** ✓ 命中\n /home/program/LiquidUnifiedDebugEngine 7 会话 ⇒ 预测 **7** ✓ 命中\n /home/program/agentmail 176 会话 ⇒ 预测 **100** ✓(实测镜像 100)\n ⇒ ★★★ 三个**独立观测值**、两个方向(>=100 被截、<100 不截)**全部吻合**\n ⇒ 这不再是\"候选解释\",而是**确证**: `session.list` 默认页大小 = 100\n ⇒ ★ 并且它 ValueError 的\"110 vs 37\"与我的\"176 vs 100\"由**同一个截断**解释\n (它测的 110 可能是**当时**该目录会话数; 现在 176)⇒ 两处对不上同源,已闭环\n ⇒ 附带: pi 五态里的 **23** 也精确命中 `/tmp/am-mcp-probe` = **23**(该目录此后未增长)\n ⇒ 五态 = 五个 directory 各自的\"min(会话数,100)\",**同一个 cap** 的多次快照 ✓\n```\n### 二★★★ pi 的 `/tmp/wt-parent` 与 `project_directory` —— **两处都不成立**(规则 ⑩,第三次同类)\n```\n 它称: \"`project_directory` 里 agentmail + `/tmp/wt-parent` 同一个 `project_id`(git_worktree)\"\n 实测:\n · `project_directory` **不是列名** —— `project` 表的列是 `id, worktree, vcs, name, icon_url,\n icon_color, time_created, time_updated, time_initialized, sandboxes, commands,\n icon_url_override`; `session` 表的列是 `directory`(**再次**:没有 `project_directory`)\n · `/tmp/wt-parent` 全库**不存在**: `session.directory`=0、`project.worktree`=0、\n `session.path`=0、`project.worktree LIKE '%wt-%'`=0、`LIKE '%worktree%'`=0\n ⇒ ⇒ 它给它\"≥8 目录\"找的**结构性解释不成立**(那条解释依赖一个不存在的目录)\n ⇒ ★★ 而**真实结构恰恰相反**: 不是\"一个 project 多 directory\"由 worktree 造成,\n 而是:\n · **一个 directory 跨两个 project_id**: `/home/program/agentmail` 的 176 条会话\n 分属 `1a8a777b…`(worktree=/home/program/agentmail, **103** 条) 与\n `1715b5c1…`(worktree=/, **73** 条) ⇒ ★ **同一目录、两个 project**\n · 一个 project 确实含多 directory,但那个 project 的 `worktree` 是 **`/`**\n (`1715b5c1…` 含 **13** 个 directory: agentmail 73、/home 9、/root 8、/tmp 7、\n /root/e2e-* 4、/tmp/oc-plugindir-test 1 …)\n ⇒ ★ 这是**\"会话在 `global`/根 project 下登记\"**,**不是** git worktree 机制\n ⇒ ⚠️ 而且 worktree 解释若成立,`project.worktree` 里应出现 `/tmp/wt-parent` —— 一个都没有\n```\n### 三★★★★★ 承重结论: 撞键的**可达性**——pi 说\"今天不撞只因 session.id 全局唯一\",我给出**更强的实测**\n```\n pi 的论证: 今天不撞 = \"session.id 全局唯一(并发集合互不重叠,是**数据性质非约束**)\"\n ⇒ 它这条**方向对**,但**论证用错了对象**: 撞键需要的不是\"id 唯一\"(PK 本来就保证跨 ws 不撞),\n 而是\"**同一个 id 出现两次、且带不同 workspace**\"\n ★ 我把这个条件直接测了(决定性):\n 对镜像每一行,取 `platform_id` 去 opencode 侧查该会话**当前** `directory`,与镜像里的 `workspace` 比:\n opencode 100 行: 一致 **100** / 不一致 **0**\n homeagent 49 行: 一致 **49** / 不一致 **0**(其 49 行全在 `/tmp`)\n dsh 80 / pi 210: 非 opencode 会话(见下)\n ⇒ ★★ **不一致 = 0** ⇒ **从未观测到任何 id 的 workspace 发生变化**\n ★ 又测\"同一 platform_id 是否注册在多个 agent 名下\"(撞键的另一种现实形态):\n 同一 platform_id 跨多 agent 的组数 = **0**; 四个 agent 的 id 集合**两两不相交** ✓\n ⇒ 与 pi 的\"并发集合互不重叠\"**实测吻合**\n ★ 又测空 workspace(撞键的**最现实路径**: `''` 与真目录算两个不同 ws):\n dsh / homeagent / opencode / pi **各 0 行**为空 ✓\n ⇒ ⇒ ★★★ **三条独立路径全部为 0** ⇒ 撞键**未在任何路径上被观测到**\n ⇒ ⚠️ 但**仍不可宣告安全**(保住我此前的边界): \n · `session.directory` 是 `TEXT NOT NULL`,**没有**任何 DB 约束禁止它被改\n · 我**未能**找到\"改 directory\"的 API 路由(strings 里只有 `/session/{id}` 等,\n 未见 move/migrate)⇒ **未找到 ≠ 不存在**\n · 且 `dsh`/`pi` 的 workspace 来自 `header.cwd`(**可为空/可变**),\n 只有 opencode/homeagent 走 `directory`\n ⇒ 结论保持: **潜在、未被观测到发生**;触发条件是\"某 id 的 workspace 在两轮上报间改变\"\n```\n### 四★ pi 的\"三重静默\" —— **两腿确认、一腿需精确化**\n```\n ① \"INSERT 无 `ON CONFLICT`\" ⇒ ★ **对**(我复核: `grep -c \"ON CONFLICT\" platform_sessions.go` = 1,\n 但那 **1 处在 `:86` 的注释里**,正文 INSERT **确实没有**)\n ⚠️ 顺带: pi 的 \"grep=0\" 与我的 \"grep=1\" **都\"对\"** —— 差在**是否把注释算进去**\n ⇒ 这本身是个**可判形状**: 数代码里的构造时,**注释会污染计数** ⇒ 应排除注释后数\n ② \"`agents.go` 降级为 -1\" ⇒ ★ **对**: `agents.go:201-207` `syncedSessions := -1`,\n 失败时**保持 -1**(不报错,注释明说\"镜像写失败只影响候选补全,不影响投递,因此不报错\")\n ③ \"桥根本不读响应(0 处)\" ⇒ ⚠️ **过宽,需精确化**: 桥**确实读**响应,但**只读它关心的字段**:\n 读: `allowed_models` / `pending_mails` / `pending_workspaces` / `alias` / `mail_id`\n 不读: `platform_sessions_synced` / `models_synced`(**opencode 与 dsh 两个桥都是 0 处**)\n ⇒ ★ 准确表述: 「**服务端回传了 `platform_sessions_synced`,而没有任何桥读它**」\n ⇒ 不是\"桥不读响应\",而是\"**这个特定信号无人消费**\"\n ⇒ 而 `-1` 与\"未上报\"**共用同一个值** ⇒ 即便有人读,也**分不出**\"没报\"与\"报了但失败\"\n ⇒ ★ 这是比 pi 那句更准的一层: **信号既无人读、又不可区分**\n```\n### 五★ 一处我自己的错(记下,与上条同类)\n```\n 我先用 `grep -c \"synced_sessions\"` 去核 pi 的 ③ ⇒ 得 0,**看似支持**它\n 但服务端实际字段名是 **`platform_sessions_synced`**(`agents.go:242`)\n ⇒ ★ 我用**猜的字段名**去核,得 0 就当成\"pi 对\" ⇒ **差点用一个错名字确认一个过宽的结论**\n ⇒ 与\"没核 from_name 就归因\"同族: **验证时用的标识符本身没核**\n ⇒ 规则 ⑩ 第三次扩射程: 核**标识符** > 核**说话人** > 核**字段名/行号/列名**都算\n```\n\n ⚠️★ **而当我把这段写进本条目时,我引的行号又错了**: 我写 `agents.go:228` 是\n `platform_sessions_synced` 的行号 —— 实测它在 **`:242`**(`:228` 落在\n \"不再回传剩余额度\"那段注释里)\n ⇒ ★★★ **我在记录\"核标识符要核字段名\"的同一条补记里,自己引错了行号**\n ⇒ 这不是巧合,是**同一个失败模式的当场重演**(写规则时又犯该规则要防的错)\n ⇒ 强化: 行号**必须写完就核**(`grep -n` 一次),不能凭\"大概在那一带\"\n" + }, + { + "id": "history-rewrite-undisclosed-citations-dangle", + "count": 1, + "kind": "**不可复现的引证**:历史被 filter-repo 重写过、没有任何 tracked 记录,旧 sha 已永久不可解释;且**没有一条判据会发现这件事**", + "due": "下一次有人要**依据文档里的 sha** 下判断或复核时(即下一次「拿引证当证据」时)。★ 到期动作**不是**去补写那些旧 sha(旧对象已 gc,补不出来),而是二选一:① 把 old→new 映射表**纳入版本控制**(现在它只在 `.git/filter-repo/` 里、永不随 clone 传递);② 承认引证不可复核,改成本仓已有的那条口径(`docs/reviews/final-check-2026-09-28.md:181`:比对 `git rev-parse HEAD`,不要把 SHA 钉进判据文件)。", + "where": "**没有判据**。实测 `client/electron/test/`+`deploy/` 里没有任何一条检查「被引用的 sha 可解析」(`grep -rl 'rev-parse --verify' client/electron/test/ deploy/` ⇒ 0 命中)。唯一相关的那条(`criteria-hygiene.test.mjs:551` 附近的 `git cat-file -e`)查的是**远端 ref 可达的 blob**(防泄露),不是「文档引用的 commit 是否存在」。", + "note": "★★★ 2026-09-30 登记。**发现路径本身就是这条债要防的东西**:我当时在核「我是不是编造过 hash」,而**没有任何判据能回答那个问题** —— 只能手搓一遍扫描。\n\n## 形状\n\n`git filter-repo` 在 **2026-09-26 09:26:41** 跑过(依据 `.git/filter-repo/commit-map` 的 mtime)。它重写了 `refs/heads/main` 的历史:\n\n· `commit-map` **817 行**:`320` 行 old==new(未动)、**`497` 行 old≠new(改了)**、**`0` 行被删**;\n· `first-changed-commits` = `7647c24abcadfb74dd172bb277747ae36a53643c` → `b806a05bfaa1430645d54ff83185c645a87a6cc1`;\n· `ref-map`:`refs/heads/main` 从 `ed4294b8597ac01dc1eb7691a72633ae656a00b6` 换成 `7c9d1cedc9cb8b095d6705f45c7aa184937c96d5`;\n· 新历史**已经推到远端**:`7c9d1ce` 是 `origin/main`(`c2796eaa52d2…`)的祖先 ⇒ 不是一次本地实验;\n· 旧对象**已经 gc 掉**,不是「只是没 ref 指」:`ed4294b` / `16a77d5` / `c155560` 全部 `fatal: Not a valid object name`。\n\n## ★ 我不知道它**消除了什么** —— 这一点必须写在最前面\n\n我第一版在这里写了「改写的目确实达成了:`dep_parser.onnx`(48.6MB)现在全历史 `0` 命中」。**那是空转读数**,我撤掉:\n\n· `git rev-list --objects --all | grep -c dep_parser` = `0` —— 但本仓**从来没有过**这个文件:`internal/nlp` 与 `server/internal/nlp` 都不存在,`internal/nlp/*` 的路径历史 **0 提交**。**0 命中区分不了「已清除」与「从未存在」**(这正是本仓反复记的「读数器没先被证明是好的」)。\n· 那个 48.6MB 是 **pi 在 2026-09-14 07:17:51 的信里提到的另一个仓/另一个议题**(它自己 09-13 的测算写的是 **46.34 MB**,两处数字也不一致)。我把它搬来当本仓的证据,是**跨域引证**。\n· 我也没有别的手段能测「消除了什么」:`.git/filter-repo/` 只有 6 个文件(`already_ran`/`changed-refs`/`commit-map`/`first-changed-commits`/`ref-map`/`suboptimal-issues`),**没有**记录被过滤的路径或表达式;改写前的 tip `ed4294b` 已 gc ⇒ **「改写减掉了多少」在本仓不可复算**。\n\n⇒ 所以本条只说**能测的那部分**:历史被换过、映射表不在版本控制里、引证因此不可复核。**改写动机与收益我一概不知道**,不替它记功。\n(可测的旁证只有一条,且**否定**「清历史是为了清大文件」这个动机:**一个 22.9MB 的 ELF(`server/server`,c401eb2 之前的误提交)至今仍可从 HEAD 到达** —— `git rev-list --objects HEAD` 里就有它,而它同时被 `.gitignore:91` 挡着、工作树里也躺着同一个 24060781 字节的文件。若改写目的在清大对象,这个漏了。)\n\n## 后果(这才是要紧的)\n\n**① 文档里所有 09-26 09:26 之前的 sha 引证,对新克隆者永久不可解释。**\n`commit-map` 在 `.git/` 内、**不被 git 跟踪**(`git ls-files` 查不到它)⇒ 它**永远不会**随 clone/clone --mirror 传递。新克隆拿到的是改写后的历史,且拿不到映射表。\n\n实测扫了全部 tracked 文件(`.md/.json/.mjs/.go/.sh/.ts`,正则 `` `[0-9a-f]{7,12}` ``,**并先扣掉邮件域**:`mails.mail_id` / `session_id` / `parent_mail_id` / `relayed_mails.relay_key` 的前 8 位):\n\n· commit 式引用(去重 file×sha)= **143**;\n· 其中**不可解析** = **98**,涉及 **89** 个不同 hash;\n· 这 89 个里,**81 个**能被 `commit-map` 的 old 列解释 ⇒ 它们是「改写前的正确 hash」;\n· **★ 8 个解释不了**:`1ca3aa8`、`3b46126`、`6d8928b`、`748a29d`、`77c15e2`、`c0852b5`、`f31bc02`、`f51c9c8`。\n 逐个查过:**8/8 都不是任何对象**(`git cat-file -t` 全空)、不在 `opencode.db`、不在任何本地克隆(扫了 82 个 `.git`,0 命中)⇒ 它们确实是「曾经是 commit、现在谁都不认识」。\n\n**② 这件事没有任何 tracked 文档记录,而文档还写着相反的话。**\n`grep -rln 'filter-repo' docs/` 在**我写这条之前**(`6010fcb`)⇒ **0 命中**。同时 `docs/API.md:8603` 写着:\n (★ 本条自己就是那个断言的第一个反例:`docs/DEBTS.json` 现在命中 1 —— 见下面 ⑲ 那段。)\n\n> **改写的代价比收益大** ⇒ 我**不改写历史**,改为在此显式标注该提交的那句作废。\n\n**与既成事实相反**。★ 更讽刺的是**这段话本身也在被重写的那段历史里** —— 它当年为「不移动后代 SHA」而拒绝改写,理由里点名的 `5c5e12b`/`79ef8c1`/`3b677ca` 现在全成了不可解析引证(这 3 个在扫描里属「filter-repo 已解释」)。\n\n**③ 至少还有一次未记录的、局部改 hash 的事件(这是假设,不是结论)。**\n`docs/API.md:11776/11840/11969` 的三行「采样: HEAD ``」记的是 **09-26 14:59:54 / 15:02 / 15:06:13**,即改写**之后 5.5 小时**,可那三个 hash 仍不可解析 ⇒ filter-repo 解释不了它们。\n另有 fingerprint 指向第二次:`09-28 08:46:01/02` **六个连续提交**(`18b148e`→`21332de`→`4556886`→`2530229`→`d4e13ea`→`1639382`,父为 `044a664`,其父 A=C=08:26:41 正常)共用**同一个 committer 时刻** = rebase 指纹;而 reflog 起点是 `09-28 09:26:15`,**晚于** 08:46 ⇒ 那一段没有任何本地记录,我拿不到那次的映射表。\n⇒ 所以我只能说「至少还有一次」,**不能说清是哪次、改了什么**。这一步刻意留成假设。\n\n## 为什么这条是债,而不是「历史事实」\n\n单看每一条,都能各自解释掉(「重写是为了减重」「旧 sha 本来就该过期」)。合成一条才看得见性质:\n\n**本仓的核心工作方法是「用引证互相复核」**(`docs/API.md` 12049 行里的 sha 就是复核的锚点),而**这个方法的前提——引证可被第三方独立复算——已经在 09-26 被单方面取消了,且取消这件事本身没有被记录**。\n仓库已有的那条警告(`final-check-2026-09-28.md:181`「不要把 SHA 钉进判据文件」)**只覆盖「SHA 会随新提交而过期」**——那种情况对一下 `git rev-parse HEAD` 就能发现、能修;**它不覆盖「整段历史会消失」**——后者**任何**事后核对都不可能,因为对象和映射表一起没了。\n\n★ 我自己的实例(这也是我这次去查的起因):我 2026-09-25 07:47:36 发 pi 的信(`13119910`)引了 `16a77d5` 与 `c155560`。现在两个都查不到。我一度怀疑**自己编造了 hash**。查 `commit-map` 才知道:`16a77d5 → fc54a816e81e`、`c155560 → b16c38ad8324`,改写发生在**发信之后 25.7 小时** ⇒ **发信时它们是对的,不是编造**。\n⇒ 若没有那张映射表,这个结论**无法得出**;而那张表不在版本控制里。\n\n## ★ 本条自己也踩了 ⑲(判据的扫描域必须排掉判据自己产出的文本)\n\n上面的 143/98/89/81/8 这组数字,**第一版算出来是 164/112/90/82/8** —— 因为我把这一段**写进 `docs/DEBTS.json` 之后又扫了一遍全仓**,于是**本条自己正文里列举的那 8 个 hash 被当成「仓库里的引证」数了进去**。\n\n修法:改成扫 **HEAD 版本**(`git show HEAD:`),而不是扫工作树 —— 即**量登记之前的状态**。\n★ 这与本仓 `criteria-hygiene.test.mjs` 里那两处排除(`:193` 排掉观察者 `read.mjs`、`:919` 按**身份**排掉被测工具自己的文件)是同一条规矩,只是换了个落点:**扫描型判据必须先划掉自己产出的文本**,否则「发现数」会随「你怎么写这条结论」而变 —— 我这次就撞上了。\n\n**量化**(实测分解,不是估):\n`docs/DEBTS.json` 里认作 ref 的**不同 hash**:HEAD 版 **11** 个 → 当前版 **32** 个,**差 = 21**,与全局那个 `164 − 143 = 21` **逐一对上**。\n这 **21 个全部来自我新写的两条**(`16a77d5`/`18b148e`/`1ca3aa8`/`21332de`/`239ff37`/`2a9be0e`/`3b46126`/`3b677ca`/`5c5e12b`/`65aacb6`/`6d8928b`/`748a29d`/`77c15e2`/`79ef8c1`/`7c9d1ce`/`c0852b5`/`c155560`/`d4e13ea`/`ed4294b`/`f31bc02`/`f51c9c8`)。\n(我那两条 note 里出现的**不同** hash 共 22 个,其中 `044a664` 是**原先就在**该文件里的 ⇒ 22 − 1 = 21,与上面的 21 逐一对上。)\n所以准确的说法是「本条使这份文件的引证数从 **11** 涨到 **32**」,而不是「本条有 19 个 hash」—— 11→32 是**文件**的增量,22 是**我两条 note** 的不同 hash 数,21 是**新增**的不同 hash 数,三个数各有各的域。\n⇒ 修法写成「扫 **HEAD** 版本」,而不是「扫工作树后再扣掉自己」:后者要维护一张「哪些是我自己写的」名单,而那张名单**又会**随我改这条正文而变。\n## 未做\n\n没有补写任何旧 sha、没有改 `API.md`(它是多会话共享的 append-only 日志)、没有动 `.git/filter-repo/`、没有把映射表复制进仓库(那是「决定要留下它」的动作,属人决定,见 `due`)。\n也没有把这条写成对 pi 的指控:pi 只是**提出过**(b)这条路,改写是谁执行的、有没有记录,我查不到。\n" + }, + { + "id": "commit-as-reply-is-not-a-reply", + "count": 1, + "kind": "**通信只进了一半**:把答复写进 git 提交(`docs/API.md`)而没有发邮件 ⇒ 对方什么都收不到,而提交信息读起来像「已经回了」", + "due": "下一次我要写「回 <某人> 」这类提交信息时。★ 到期动作:**要么同时发信,要么把提交信息的措辞改成不冒充答复**(例如 `docs: 记 的复核结论(未发信)`)—— 现在这两种情况在 git log 里**同形**。", + "where": "**没有判据**。提交信息与邮件是两套互不校验的通道:`git log` 里 `回 pi ` 形式的提交**没有任何东西**检查那个 `` 是否真有对应的出站邮件。", + "note": "★★ 2026-09-30 登记(我自查「我还有哪些没回」时实测发现)。\n\n## 形状\n\n用「回 pi ``」这类**看起来像答复**的提交信息,把答复只写进 `docs/API.md`,**没有调用 `send_mail`**。对方(pi)那侧**不会收到任何东西** —— 这正是本会话每轮 harness 提醒的那句:「把话说完并不会让对方收到任何东西」。\n\n## 证据(实测,两条独立通道交叉核过)\n\n全仓用「回 pi ``」形式的提交只有 **3** 笔。用 `parent_mail_id` 交叉核对(**权威判据**,不是按正文找字符串):\n\n| commit | 时刻 | 指向 pi 的 | 是否配了真邮件 |\n|---|---|---|---|\n| `2a9be0e` | 2026-09-26 03:44:02 | `cc7a3027` | ✓ 有 |\n| `239ff37` | 2026-09-26 09:15:44 | `e77154d1` | **★ 无** |\n| `65aacb6` | 2026-09-27 04:02:38 | `4b4dd2c7` | ✓ 有 |\n\n⇒ **3 笔里只有 `239ff37` 没配邮件**,所以这不是「约定」,是**漏发**。\n\n`239ff37` 的正文(`docs: 回 pi e77154d1 —— 撤回 T\\L 探测器形状(修后必假阳);(d) 拆 d1/d2,d1 已落地为真判据`)**54 行**写进 `docs/API.md`,内容完整:§二收(`T\\L` 根因错在「即将被销毁」由 DELETE 谓词决定)、§三收(d1 该现在就建)、以及一处我自查出的探针 bug。\n\n而 `e77154d1`(09-26 03:56:41)到今天我核时 **`parent_mail_id` 子信 = 0** —— 它等了 **4 天**。我最后一封真邮件是 `f633a870`(03:54:08),**早于**它 2 分 33 秒。\n\n## ★ 我自己的代理判据在这件事上也是错的(一并记)\n\n我第一版用「`parent_mail_id` 无子信 ⇒ 未答」来筛「还有哪些没回」。**它是错的**,有**假阳**:\n\n· pi 在会话 `21c398ee` 共 **139** 封来信;\n· 按「无子信」判 = **33** 封;\n· 其中 **9 封语义上其实已答**(`eac0523d`、`5fe02fea`、`5377da95`、`908665ef`、`d8dce3a5`、`ffcebfc3`、`3ed20be1`、`3beda7e2`、`48c2c4c9`);\n· 真正未答(语义判据:其后有我的信、且**父链指向它或正文点了它的 id**)= **24** 封,其中 09-25 之后的 **10** 封。\n\n机制:**一次答复多封时,`parent_mail_id` 只能指向一封**(例如我那封标题就是「三封一起回…」)。⇒ 「子信数 = 0」把「被批量回过的信」判成了「没回」。\n\n★ 这是本会话第 N 次同族错:**代理判据(child 计数)与语义(答复)域不同**,而我没有先找一个「代理说 A、语义说 ¬A」的实例就用了它。这两个数(33 vs 24,差 9)就是那个实例。\n\n## 未做\n\n· **没有补发那封信**:`send_mail` 被会话级闸挡下(「本会话已连续 268 封 Agent 之间互相回信、其中没有任何人类参与(上限 8)」)⇒ 补发不是我能自行完成的动作,需要人类在会话里插一句话让计数归零,或明确豁免本轮。\n· 没有把那 24 封逐封回掉(那是刷邮件,正是那道闸要防的)。\n· 没有复核「语义判据」本身是否还有假阳/假阴(它按 id 子串匹配正文,理论上会**误判引用为答复** —— 例如我引用 pi 的 id 来否定它,也会被算成「答了」)。这条我**没测**,所以上面 24 这个数应当读作「上界」,不是精确值。\n" } ] }