|
|
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 |
|
|
|
018d5b3bd8
|
test(桥): 四个桥的 relay-policy 必须逐字相同 + 钉住 in_reply_to 缺方向判据
## 新增 client/electron/test/cross-bridge-prompt.test.mjs(5 条,登记进 SUITE)
`relay-policy.js` 是 pi/dsh/zcode/opencode **各存一份的手抄副本**(当前
四份 md5 相同),它直接决定提示词里对模型说的话。修 `in_reply_to` 那一族
要改四个地方,**漏一个就会让分叉活到线上**。本判据就是防那个。
与 `cross-client-logic` 的分工:那边比**行为**(electron/harmony 两套类型
系统,只能跑同一张表比结果);这边比**字节**(同一个 node 运行时下的四份
JS 拷贝,没有任何语言差异要归一 ⇒ 字节相等是最便宜也最严格的判据)。
枚举挡实例、行为判据挡漂移,两者配对。
## 5 条的形状
· 4 条绿:四份 `relay-policy.js` + 四份 `relay-policy.test.mjs` 逐字相同,
且四个桥都存在(少一个即部署事故,当场红)。
★ 已实测它**有牙**:往 dsh 那份尾部加一行注释,判据立刻红并指名
`✗ dsh sha12=…`(不是笼统说"有分叉"),随后已还原、四份 md5 复验一致。
· 1 条**故意红**:`inboundHeadline` 不得只凭 `inReplyTo` 非空就宣称
「你上一封信的回复到了」—— 按形状断言(找方向判据字段),不点名实现。
这条红的就是 `docs/DEBTS.json` 的 `in-reply-to-ignores-direction`:
压测线索 `stress-thread-21863-15348` 里 8 封全是 `opencode → pi`,
投递通知却逐封宣称「回的是你那封:<上一封的 id>」,而没有任何一封是
pi 发出的。单向续信链同样满足「有父邮件」⇒ 纯单向的链被读成双向对话。
★ 判据先写好、修完转绿,不写就永远没人知道还欠着 —— 与本仓
「先钉判据再修」一致;到期动作不是「在提示词里写清楚」(本次已证明
写清楚没用:通知里逐字写着那句,模型照样每封去核一遍再被带着走)。
## 验证
· `node test/run-all.mjs`:files=35 ran=35 checks=582 pass=571 fail=2
**skip=9(不是通过)** red=2。
两个红:① 本文件那条故意红的;② `build-stamp`(BUILD_INFO 记 87c55ac
vs HEAD 359cb43)—— ★ **已在干净 HEAD 上复现,不是本次引入**,
与 `shared-workspace-unserialized-deploy` 同一形状(产物与源码分家)。
· 手改 SUITE 登记数字 5(套件只判下界,不手改将来删掉就不红)。
· go 侧 16/16 包绿(须 `GOCACHE=.tmp/gocache`,默认 `/root/.cache/go-build`
权限被拒)。
## 我自己踩的一个坑(第一次跑就撞上,已修)
初版裸 `readFileSync` ⇒ `criteria-hygiene` 第 2 条红
(「判据目录里不得出现裸 readFileSync」)。已改走 `prose()`。
★ 讽刺处:**本判据主题正是"手抄副本会分叉"**,而我第一版就制造了
一处新分叉(多写一个 import)。这条债说的就是这类形状。
|
2026-09-28 10:17:09 +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 |
|
|
|
35557d4f8d
|
test(pi桥): 补两格判据 —— BoundedSet 淘汰使「重启才丢」的前提站不住
`markDelivered` 的注释写「标不上不该让投递失败……代价只是下次重启可能再投
一次」。opencode 独立复核指出这句话的前提站不住,我认这个判断。
「下次重启」把正确性押在「重启 ⇒ 内存全丢 ⇒ 从库里重来」上。但
`deliveredMails` 是 `BoundedSet(MAX_TRACKED_MAILS=2000)`,**运行期就主动淘汰**,
不需要重启。于是有一条更窄的重投路径:
投递 X ⇒ add(X) → POST /mail/read 失败(库里仍是 unread)
→ X 超过 2000 被淘汰(内存忘了,库没忘)
→ catchUp 按 unread 捞回 X,内存 has() 挡不住 ⇒ 重投
正是 2026-09-26 那个症状本身(重投回声),只是窗口窄得多。
两格分别锁住前提与后果,都不靠注释断言:
⑤ `deliveredMails 确实是有界的` —— 从 index.mjs 取上限常量名,回 bounded.js
取其值并断言有限。有限 ⇒ 运行期会淘汰 ⇒ 「靠重启才丢」不成立。
⑥ `淘汰只丢内存、不回写库` —— 覆盖 BoundedSet.add 的整个淘汰循环,
断言其中无 /mail/read|markDelivered|post|status|client;并对照
markDelivered 的失败分支只打日志、不重试不回滚。
将来若让淘汰也落库,这格会红,提醒改的是注释而不是加静音。
变异自测(三个变异各被对应格抓住,非自说自话):
改成无界 `new Set()` → ⑤ 红
淘汰路径里回写库 → ⑥ 红
markDelivered 失败后 setTimeout 重试 → ⑥ 红
判据自带的两个坑留在注释里(第一版「怎么变异都不判红」的原因):
必须锚在 BoundedSet 的 add 上,否则裸 /add\(value\)/ 会先命中文件前面
BoundedMap 的同形 add;淘汰循环要带尾巴({0,320}? 惰性量词会在第一个终点
就停),否则紧跟其后的落库代码永远落在窗口之外,结构上不可能判红。
全量 526 格通过。未修 —— 本提交只把已知窗口钉成可判红的判据,
不动 `markDelivered` 的行为(改行为是另一次决定,且应先决定淘汰时
要不要落库)。
Co-Authored-By: pi <pi@agentmail>
|
2026-09-28 09:57:41 +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 |
|
|
|
bfc9d87b24
|
test(push): 补 pi 建议的那一格 —— DailyLimit=1 时失败后仍要能发
上一提交补的是**内部计数器**(dayCount == 0)。pi 在报告里建议的是
**用户看得见的行为**:DailyLimit=1,一次 accessToken 失败后第二次
仍然要能发出去。
两层都要钉的理由:计数器对而行为错是可能的 —— 那会让运维收到
「达到每日推送上限」这种**误导性文案**,真实原因却是上一次网络抖动。
变异验证:把 reserveDaily 挪回 accessToken 之前 ⇒ 两格同时红。
--- FAIL: TestHMSAccessTokenFailureDoesNotBurnQuota
--- FAIL: TestHMSQuotaSurvivesTokenFailureWithLimitOne
|
2026-09-28 09:50:24 +08:00 |
|
|
|
186cf53804
|
fix(push): 额度预留**真的**挪到 accessToken 之后(上一版只写了注释)
## 起因:pi 在邮件驱动的一轮里当场抓出来的
pi 收到那封 `[收尾验证]` 邮件后,自己翻代码核对,
在会话文件里写下(原文):
The code contradicts its own comment (item ②: reserve should be *after* accessToken)
The commit only changed the argument (`len(tokens)` → `1`) and the comment — it
Fix ① (per-batch) is real and tested. Fix ② is claimed but not implemented.
Confirmed — the bug is real.
它甚至自己造了探针(`zz_probe_test.go`,跑完已删)来实证。
**我独立复核确认它是对的**:
`reserveDaily(1)` 在第 212 行,`accessToken` 在第 215 行 ——
预留仍在**之前**。2026-09-26 那次我只改了 ①(`len(tokens)` → `1`),
把 ② 写进了注释,**代码没动**。
## 为什么当时那批判据没接住
`push_test.go` 原有 3 格只验 ①(按批次计),**造不出「accessToken 失败」这条路** ——
`hmsStub` 的 `/token` 永远返回 200 + 令牌。
⇒ 「注释说修了」与「代码真修了」能分家,而没有任何东西会发现。
## 改法
① `hmsStub` 加 `failToken` 开关(`/token` 可返回 400)。
② `reserveDaily(1)` 挪到 `accessToken` 成功**之后**、真正发请求之前。
仍保持**前置预留**语义(不是"发成功后再扣")—— 那会超发,
并发下多个 goroutine 都能通过检查。宁可少算也不多发。
③ 新增 `TestHMSAccessTokenFailureDoesNotBurnQuota`:三次 accessToken 失败后
断言 `dayCount == 0`、零推送发出、且恢复正常后仍能发(额度没被吃掉)。
## 变异验证(这格判据本该在 2026-09-26 就存在)
把 `reserveDaily` 挪回 `accessToken` 之前(= 还原成 bug)⇒
★ accessToken 失败不该扣额度,实际已扣 3 条
(一次网络抖动静默烧配额就是这么来的)
## 教训(与本仓 python-probe-shadowing / baseline-residue 同族)
**「我写了注释说明怎么修」不等于「我改了代码」。**
审查报告给了两条,我处理了一条,把另一条**誊进了注释**就当做了。
写完注释应当立刻核对行号 —— 那是 5 秒钟的事,而这次是别人替我发现的。
★ 另一层:**别人(或另一个 Agent)独立复核出来的结论,要自己再验一遍再改**。
我逐条查了行号才动手,没有因为"pi 说的"就直接信。
|
2026-09-28 09:49:22 +08:00 |
|
|
|
83a5787ac9
|
fix(homeagent桥): SDK 钉子指向 v1.3.0,而构建机指针已是 v1.4.0
## 现象
`go test ./...`(在该包内)红:
--- FAIL: TestSDKPinMatchesBuildMachinePointer
go.mod 的 SDK 钉子偏离了构建机指针:
钉子: /root/.homeagent/hmapdev/sdk/v1.3.0
指针: /root/.homeagent/hmapdev/sdk/v1.4.0
后果不是「编译不过」,是**照原样重编会再造一个旧版本插件**:
内核升级后装上去启动即崩、崩溃循环、要人介入(2026-09-14 的形状)。
## 为什么这个包的红一直没被发现
`plugins/homeagent-mail-bridge/` 有**独立 go.mod** ⇒ 不在主 module 内
⇒ 仓库根的 `cd server && go test ./...` **覆盖不到它**。
(主 module 覆盖率实测:`go list ./... | grep -c homeagent` → **0**。)
## 改法
`go.mod` 的 replace 改成指针指向的目录(判据给出的改法,一行),
然后按 `build.sh` 重编并部署。
## 验证(判据自己写明的口径:不是版本号相等,是建链成功)
- 判据:✅ 过
- 重编:`hmapdev build` 成功,`SDK=v1.4.0 hash=ba5cece0ca8a52ad version=1.4.0`
- 部署:`install -m 0755` 原子替换 + 留档 `plugin.bin.bak-20260928-093600`
- **建链**:内核日志 `homeagent-mail-bridge` 经 proc 通道加载、
`protocol=2 sdk=1.4.0`、工具全部注册(`loaded: homeagent-mail-bridge`)
- **端到端**:真发一封 `.new` → 20s 内回信「SDK v1.4.0 重建后正常」
★ 顺带记一条:build.sh 自己注明 `go build ./...` 会报
`function main is undeclared` —— 那是正常的(入口由 hmapdev 包装),
真正的编译在 hmapdev 那一步。拿 `go build` 当"能不能编"的判据会误报。
|
2026-09-28 09:40:34 +08:00 |
|
|
|
093c4dd06e
|
fix(桥接): 模型清单过期时**起会话前**就剔掉(opencode 每轮白烧一次)
## 现象(线上实测,不是推演)
[mail-bridge] 模型 opencode/mimo-v2.5-free 失败:
Model not found. Did you mean: mimo-v2.6-flash-free, ling-3.0-flash-fin-free...?
[mail-bridge] llmsproxy/AUTO 成功(前 1 个失败)
30 分钟内 9 次 —— **每一封邮件**的第一轮尝试都是它。
## 根因
`agent_allowed_models` 里 opencode 的 rank=0 仍是 `mimo-v2.5-free`,
而**上游已改名** `mimo-v2.6-flash-free`。查证:225 个 provider 的实时目录里
确有 `mimo-v2.6-flash-free`、无 v2.5;库里 `agent_model_catalog`
(心跳上报的快照,今天 01:28)也已含新名字。
⇒ 清单是管理员存下来的,**没有任何失效检测**。降级逻辑救了它(信还是回了),
但代价是**每轮白烧一次 + 延迟翻倍 + 一条永久错误日志**。
## 为什么不是「在服务端过滤掉」
`ListAllowedModels` 的注释明确否掉了这条路:
「不与目录做 JOIN:目录是平台上次注册时的快照……在这里用目录过滤,
只会把『目录暂时没上报但其实可用』的模型挡掉」。**这个判断是对的**,不改。
## 改法:桥侧预检(pi 桥早就有)
`pi-mail-bridge/src/worker.mjs:674-680` 同一件事已经做了,注释写着
「目录里根本没有这个路由:**同步就能判定,不必起一轮**」。
本提交把那条纪律提到共用层 `model-scope.js` 的 `partitionByCatalog`,
让 opencode / dsh / pi 共用。
**实测对比**(同一封信,修复前后):
修复前:模型 mimo-v2.5-free 失败: Model not found… ← 起了一轮会话才失败
修复后:跳过 opencode/mimo-v2.5-free:平台目录里没有… ← 同步拦下,零会话
llmsproxy/AUTO 成功
## ★ 最要紧的一条:拿不到目录时**全部放行**
心跳还没跑过 / 拉取失败 ⇒ 目录是 `undefined`。此时若照样剔除,
Agent 会**彻底哑掉** —— 而失效方向恰是「什么都收不到」。
与 `reportModels` 拉取失败时**省略字段而不是传空数组**同一方向。
判据里对 `undefined / null / [] / 'not-an-array' / 42` 五种输入逐个断言。
`routes` 被剔空时**退回平台默认**并打日志说明「请到管理页重新划定范围」——
否则发件人只看到「本次未能处理」,而管理员看不出自己的选择已过期。
## 判据(并入共用测试,三桥同源,33/33 过)
7 格,含:线上那个 case、拿不到目录必须全放行、`undefined` 路由不归目录管、
全失效时 kept 为空(退路是策略决定不是过滤职责)、provider 同名 model 不同不算数。
变异验证两向都打红:
① 去掉「拿不到目录就全放行」的保护 ⇒ ★那格红
② 键只用 provider(半匹配) ⇒ 4 格红
|
2026-09-28 09:40:15 +08:00 |
|
|
|
c851bef5cb
|
fix(4 bridges): 投递提示词改指向 read_mail,不再引 read_inbox
投递时 `markDelivered` 就把信标成已读(dsh `src/index.ts:505` 定义,
`:551`/`:2214`/`:2276` 三处调用,SSE 主投递与 catchUp 都走它 ——
2026-09-26 为治"重启重投→回声"定的性)。而提示词却要求"先调
read_inbox 读正文",read_inbox 默认 `status=unread` ⇒ **看不到刚投递的
那封信**。
危险的不是"白费一次调用",是模型看到空之后以为"没有新邮件"就结束
回合 —— 那会**静默丢掉一个真实请求**。mail_id 就在同一段提示词里。
## 改了什么
投递提示词(8 处)与 read_inbox 工具描述(4 处):
请用 read_mail(mail_id 用上面「邮件 ID」那处) 读取这封邮件的完整正文……
这封信在投递时已标为已读,而 read_inbox 默认只看未读,读不到它;
read_inbox 只用来看本会话的其它未读。
工具描述那句「收到新邮件通知后应立即调用此工具」是**模型看到的第一句话**,
只改提示词不改它,模型照样走偏,所以一并改。另给 read_mail 的描述补
一句「刚投递到本会话的那封邮件就读这个」。
落点(dsh 桥是**三处**,不是一处 —— adoptPrompt / resume 复用 / 新开会话
三条建会话路径各带一份文案):
- dsh `index.ts:1047`、`:1163`、`:1219` + 工具描述 `:1428`/`:1680`
- opencode `index.js:1016` + `:261`/`:473`
- pi `turn.mjs:170` + `tools.mjs:197`/`:430`
- zcode `prompt.mjs:152` + `lib/tools.mjs:123`
## 为什么指向 read_mail 是安全的
`workspace` 必需只加在**两个**端点上:GET /mail/inbox
(`server/internal/handler/mail.go:626`,用户裁定:不带 workspace 是错误
发件格式)与全部标已读(`:806`)。`read_mail` 走
`GET /agent/mail/{id}` → `AgentGetMail`(`agent_discovery.go:306`),
该路径**没有 workspace 参数**,鉴权走 `canReadSession()` 按会话归属判定。
桥侧也印证:`read_mail` 走 `withScope()`(只拼 session_id),
`read_inbox` 的 scope 单独拼 `&workspace=`。**两条路分开,400 搬不过去。**
## 不动默认行为
`read_inbox` 的默认 `status=unread` 保持不变(`lib/inbox-format.js:157`
刻意如此:默认 all 会让模型每轮重读旧邮件),`idsToMarkRead` 在 `all`
下不标已读与"投递即标已读"配套。改默认值等于把 2026-09-26 定的性放松
回去,不是另一种修法。
## homeagent 故意不动
`plugin.go:673,1005,250` 三处留着。改 Go 源码需重出 `plugin.bin`,产物在
**跨机器**的 `/home/newqqagent/plugins/`,你我都碰不到 —— 源码改了线上
没生效,仓库与线上分叉比不改更难排查。待跨机重出。
## 测试
新增 test/inbound-prompt-reads-mail.test.mjs(5 项),钉住:旧文案不存在、
新文案三处齐全、工具描述改过、read_mail 描述指过、附件指引保留。同时
断言 src 与 **dist**(dist 是 dsh 真正加载的那份,忘了 build 就是
"源码对、线上旧代码")。
四桥测试:dsh 419 / opencode 344 / pi 517 / zcode 394,全绿。
(改前 407/344/517/394,无回归。)
## dist 变更(不在 diff 里,.gitignore:20 忽略 plugins/*/dist/)
重出后 `请先调用 read_inbox` 由 **3 处 → 0 处**,新文案 3 处;
旧工具描述 1 处 → 0 处。已用 `npx tsc -p tsconfig.json` 重出。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-09-28 09:29:26 +08:00 |
|
|
|
9985c41b13
|
fix(dsh-bridge): 新开会话登记工作区,workspaceOf 加淘汰兜底
read_inbox 在**新开会话**里恒 400("缺少 workspace"),而 read_mail /
read_thread / list_contacts / session_participants 都不受影响。
## 根因
`workspaceOf()` 取不到工作区就返空,URL 不带 `&workspace=`,服务端 400
(`server/internal/handler/mail.go:626`,用户裁定:不带 workspace 是错误
发件格式)。而 `sessionWorkspace` 只有两个写入点 —— adopt(bindAdopted)
与确定性 id 恢复 —— **新开会话分支漏了**。
漏了之后只有两种情况能发现:那条会话后来恰好走过另外两条绑定路径,
或者重启后 `sessionWorkspace`(500 条上限)把它淘汰掉。
## 改法
1. 新开会话分支补 `sessionWorkspace.set(attemptSessionId, sessionCwd)`。
★ 取值必须是 `header.cwd`,**不是那个 `cwd`**:后者来自
`resolveWorkspaceCwd`,`to_workspace` 不可用时回退到
`mailSessionFallback` → `~/.dsh/mail-sessions/mail-<uuid>`,每次邮件
都不同。拿它登记会把收件箱收窄到一个**永远读不到信**的目录 ——
比 400 更坏,因为 400 至少是可见的错误。同一文件下方工作区注册那段
(`actualCwd`)也是从 header 取,两处保持同源。
2. `workspaceOf()` 读侧兜底:反查 `sessionMap` 的 `directory`。
补在读侧而不是写侧,因为淘汰是**事后**发生的:补写站点只能覆盖
「我这次走过」,被淘汰的键下次谁来读都读不到。反查的 `directory`
在两条绑定路径上都是真实 cwd(adopt 走 `persistedCwd` 读磁盘
header),不是兜底目录。
## 影响面
`read_inbox` 的两维收窄(session_id + workspace)是**防越界读**的机制
(2026-09-14 用户报的越界)。这个改动让更多会话能拿到 workspace,因此
**可见范围确实变宽**了 —— 这是有意的(原本是读不到,不是读得少),
但复核时应当盯住这一条。
## 测试
新增 test/workspace-of-fallback.test.mjs(7 项)。其中 5 项把
`workspaceOf` 的真函数体抠出来在真 BoundedMap 上跑(不复制一份实现,
否则源文件改了测试还在绿)。已做变异验证:删掉读侧兜底 → 3 项红;
删掉写入点 → 2 项红。
dsh 桥 414 项全绿(改前 407,无回归)。
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
2026-09-28 09:26:15 +08:00 |
|
|
|
c2796eaa52
|
fix(部署判据): 悬空软链不再让第 ① 条变成「不可达检查」+ 补上它指向哪里
## 起因
第 ① 条("没有任何 unit 引用源码目录")在本机**长期红**,note 写的是
「读不到(这几条没被检查):…/sysinit.target.wants/mdadm-shutdown.service(ENOENT)」。
查下去:那是**系统自带的悬空软链**(mdadm 未装 ⇒ 目标不存在),
不是本仓的部署事故。但判据仍然红,而它红得**没道理可讲**。
## ★ 真缺陷:那个软链分支是**不可达代码**
/etc/systemd/system/sysinit.target.wants/mdadm-shutdown.service
→ /lib/systemd/system/mdadm-shutdown.service (不存在)
`readFile` 对悬空软链抛 ENOENT ⇒ 走 `catch { …; continue; }`
⇒ **下面那段专门判软链的代码永远不会执行** ——
而那恰恰是对软链该做的检查(看**目标位置**)。
⇒ 这不是"误报",是**一个该跑的检查没跑**。判据没回答"这个软链指向仓库吗",
只回答了"我读不到它"。
## 修法
`readlink` 只读**链接自身**的内容(那个路径字符串),**不要求目标存在** ——
这正是悬空软链唯一还能回答的问题。
现在:既把目标查出来并判(指向仓库 ⇒ 进 `repoLinks` 报红),
又**仍然记进 `unreadable` 继续判红** ——
「没能检查它的内容」≠「它没问题」,这条不能因为解释清楚了就不算。
## note 里单独一句「悬空软链(**非本仓违规**)」
它与「真有 unit 读不到」要处理的人不同:
前者是装软件留下的系统状态,后者可能是部署事故。
混在一堆里 ⇒ 读的人不知道该不该管。
## 残余(有意保留)
第 ① 条**仍然红**,但现在红得**有理由且已说明**。
要让它转绿只能二选一:装 mdadm、或删掉那个悬空软链 ——
**都不是本仓该做的事**,所以不为了转绿而放宽判据。
自检 48 格全过(本次未新增样本;这一格的真机样本就摆在那里,
且 `--self-check` 的假 fs 走不到 `sysinit.target.wants` 这条路径)。
|
2026-09-28 08:49:40 +08:00 |
|
|
|
1639382eaa
|
fix(部署判据): ⑤ 的口径从「数子串出现次数」改成「只认源码**文件路径**」
## 起因
trimpath 修好之后(67 处 → 0 处真源码路径),判据第 6 条**仍红 1 处**。
查下去发现那 1 处不是缺陷:
server/internal/handler/mail.go:645 的 400 错误文案 ——
「请带上你所处工作区的绝对路径,例如 &workspace=/home/program/agentmail」
那是**给调用方看的示例值**,且它在二进制字符串表里紧邻下一条 SQL 字面量,
拼成 `…/agentmailINSERT INTO mails (session_id, …)` ——
**看起来极像「路径 + 代码」,实际是两条无关的字符串常量相邻**。
## ★★ 我上一轮把它误判成「测试夹具」
我grep 源码时命中的是 `notify_test.go` 里的 workspace 夹具,
就下了「是测试数据」的结论 —— 那是**另一个**字符串(`seedAdopted(t,"pi","pid-ws-1",
"/home/program/agentmail")`),只是恰好也含 REPO。
**真正的来源是生产错误文案。** 先下结论再取证,又一次。
## 口径改动
原口径 `text.split(REPO).length - 1` 数的是**子串出现次数**,把两件事混成一件:
· 真缺陷:trimpath 没生效,产物里印着 `…/server/internal/repo/repo.go`
· 误报: 源码里**本来就该有的字符串**恰好含这个子串
⇒ 改成逐个出现位置看**后缀**:REPO 之后是**源码文件扩展名**才是真路径。
这条判据要抓的是"源码**文件位置**被泄露",扩展名正是它的形状;
而示例值后面跟的是 `INSERT`(SQL 关键字),不是文件。
## 两条都要报(把两者混成一个数字正是原口径的毛病)
bad = 真源码路径 ⇒ **判红**
other = 还有别处出现但不是文件路径 ⇒ **只提示**,且**给出真实样例**
第一版的"只提示"那档输出了「样例:见下」而样例永远取不到值
(只给真路径留了样例)—— 一句指向不存在内容的指路词。已修。
## 验证(两侧都用**真 26MB 二进制**,不是合成样本)
已部署(-trimpath) 源码路径=0 非文件字样=1 ⇒ 判绿 ✓
本地构建(无 trimpath) 源码路径=66 非文件字样=1 ⇒ 判红 ✓
## 自检
新增一格反面样本:`★网关二进制:非源码路径的仓库字样(示例值)不得误判红`
(`--self-check` 47 → **48** 格)。
变异验证:把 `SRC_EXT.test(tail)` 改成 `true`(退回数子串)⇒
该格**打红**且整套自检报"检查器本身不可信" ⇒ 新格确有分辨力。
|
2026-09-28 08:46:02 +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 |
|
|
|
21332de5e7
|
test(判据): criteria-hygiene 的 AGC 探针自检改用临时仓合成对照
## 为什么要改
那条判据("AGC 真身从未进过远端历史")的自检原本要求
**本地可达历史里确实有该路径**,用它证明 `git log -- <路径>` 这套查法可用。
★ 那个前提**已经不成立**了:真身**从未被提交过**
(`client/harmony/.gitignore:26` 一直在挡它),所以本地历史里
本来就查不到 ⇒ 这条判据**永久红、且无法自查**。
(注释里引用的 `7647c24` / `320c93f` 在本树也**不存在**。)
而判据的分诊早已确认:真身确实从未进过历史(被 ignore 正确挡住),
红的是**探针的假设**失效,不是缺陷存在。
## 改法:合成阳性 + 阴性对照
在**临时仓**(`mkdtemp`)里造两个提交 —— 一个含待查路径、一个不含 ——
对两者跑同一套查法,断言**双向有分辨力**。临时仓不碰本仓任何状态
(`GIT_CONFIG_GLOBAL=/dev/null` 避免读用户配置),造完即删。
为什么**必须**有阳性对照:一条用来抓泄露的判据,
**正确工作**时恰好永远看到"空"(没泄露 ⇒ 查不到)。
**"真值恰好是空"与"查法坏了"在输出上同形**(都是空串 + exit 0)
⇒ 真实历史里没有阳性样本可用,只能现造。
## ★ 阴性对照第一版写错了(变异测试打出来才发现)
我先写成查一个**真实存在**的无关文件 `unrelated.txt`。
变异把它改成查阳性那个 `leaf.json`,判据**照样绿** ——
因为查 unrelated 本来就该命中,那不叫"查法在乱报"。
⇒ 阴性对照要证明的是「**不存在的**目标查不到」,也就是**查法有边界**。
改成查 `no-such-file-ever.json`,并**额外**验一次 sanity
(真实但无关的文件**应当**查得到)—— 两个方向都对才算有分辨力。
改后双向变异都能打红(阳性查不到 ⇒ 红;阴性恒命中 ⇒ 红)。
|
2026-09-28 08:46:02 +08:00 |
|
|
|
18b148e476
|
跨端: fix(客户端) 换身份必须清全部账号数据 + 鸿蒙管理台门禁改三态
两份审查报告(`docs/reviews/electron-gui-review.md` /
`harmony-client-review.md`)里两条**数据隔离**缺陷。
## ① 换身份不清数据 ⇒ 在新账号的界面下显示旧账号的邮件
`setActive` 之后 `api/config` 单例里的 API_BASE 与 bearer 就翻到了新账号,
于是此后每个请求都带**新账号**的凭证。而各 store 里还留着**旧**账号的:
· mailStore.sent / currentMail
· sessionStore.sessions / currentSession / currentSessionMails
· contactStore.contacts / archivedContacts
⇒ 肉眼完全看不出来(不报错、不空屏),而**从旧视图发出的写操作**
(归档 / 转发 / 批准权限)改的是**新账号**。
新增 `src/lib/resetAccountData.ts` —— **一处实现,三个入口都调它**:
① 主动切账号(AccountSwitcher.pick)
② 登出(authStore.logout)
③ 任意接口 401(api/client.ts 的 unauthorized 回调 → markAnonymous)
只在 ① 里清是最容易漏的那种做法:② 和 ③ 各自还会重新泄露一次,
而它们都不在切换账号的代码路径上,grep 也找不到。
**身份变化有三条路径,清空也该有三条。**
★ 只碰**数据** store;`uiStore` 的 reset 仍由 App.tsx 负责
(它还要复位窄屏分栏、写信态那些纯界面状态)。
判据:`test/stores/resetAccountData.test.ts`(4 格)。
## ② 鸿蒙管理台门禁**失败开放**(fail-open)
`AdminUsersPage.ets` 原来的条件是 `roleKnown && !this.isAdmin`,
于是 `roleKnown === false`(loadRole() 失败、**身份还没读到**)
落进 else 分支 ⇒ **把完整管理台整个渲染出来**。
一次网络抖动 = 管理入口对所有人可见。
讽刺的是该文件自己的头注释写的就是正确规则
(「不能把读不到当成是管理员」)—— 代码做的正是这条注释禁止的事。
⇒ 改三态:`!roleKnown` 显示「正在确认身份…」、`!isAdmin` 显示墙、
否则管理台。
服务端 `middleware/user.go` 的 `AdminOnly` 仍在,所以**不是越权**;
但非管理员会看到完整用户列表、建号表单、改密入口 ——
属于客户端信息泄露 + 无意义的失败请求风暴。
|
2026-09-28 08:46:01 +08:00 |
|
|
|
044a664cc3
|
fix(push): HMS 每日额度按**批次**计,且挪到"确认能发"之后
审查报告 `docs/reviews/push-and-gui-review.md` §二.1 记的两条,都在**线上**
(已部署二进制是 f51c9c8 的构建,此修复未上线)。
## ① 计数单位错:按 token 数扣,变量名与文案都说"条"
`hms.go` 原先 `h.reserveDaily(len(tokens))`,而变量名 `dayCount`、
注释、报错文案(「达到每日推送上限 N **条**」)说的都是"条"。
华为的测试消息额度是按 **`messages:send` 的调用次数**计的
(一次请求一条消息,无论 `message.token[]` 里有几个设备)。
⇒ **3 个设备收到 1 封邮件就吃掉 3 条额度,实际只发出 1 条。**
多设备自部署用户会按 1/设备数 的速度提前耗尽 1000 条/天。
修法:`reserveDaily(1)` —— 一次 `Send` = 一条消息。
## ② 扣在投递**之前**:一条都没发出去,额度却已经扣了
原顺序:reserveDaily → accessToken → HTTP 请求。
`accessToken` 失败 / HTTP 失败 / 华为回非成功码,这三种情况
**一条都没发出去**而额度已扣,且失败只 `log.Printf`
⇒ 一次网络抖动静默烧掉配额。
修法:挪到 `accessToken` **之后**、真正发请求之前。
★ 为什么不是"发送成功后再扣":那会超发(并发下多个 goroutine
都能通过检查)。保留前置预留、但放在"确认能发"之后,是
**宁可少算也不多发**的取舍 —— 少算的代价是偶尔一次失败
没计入,超发的代价是真超额被华为拒。
## 验证
`server/internal/push/push_test.go` 补 3 格(+27 行):
按批次计(多设备一封邮件只扣 1)
accessToken 失败**不**扣额度
reserveDaily 的单位是"条消息"而非 n 个 token
|
2026-09-28 08:26:41 +08:00 |
|
|
|
e2472287f0
|
fix(sse): Client 加写锁 —— 同一 ResponseWriter 被并发写(-race 证实)
## 缺陷
`internal/sse/manager.go` 的 Client 结构体**一把写锁都没有**,
而 `Manager.mu` 只护 `clients` map 的**遍历** —— 遍历期间对每个
client 的 `c.SendWithID` 是**并发**的。
`http.ResponseWriter` 不是并发安全的,而 SSE 又是文本协议
(`id: N\nevent: X\ndata: {…}\n\n`),两个 Fprintf 交错就把
data 的 JSON 劈成半截 ⇒ 客户端 EventSource 收到坏帧、丢邮件。
## 实测(真实 httptest.ResponseRecorder + -race)
WARNING: DATA RACE
Read at ... by goroutine 13:
net/http/httptest.(*ResponseRecorder).writeHeader()
sse.(*Client).SendWithID() manager.go:350
★ 32 goroutine × 25 帧 = 800 帧,只切出 459 帧完整
## 生产上会打中的三条路径
① handler/permission.go:412-414 —— `SendToAgent(perm.AgentName,…)`
紧接 `SendToUser(user.Username,…)`,两个不同 HTTP 请求命中同一账号。
② 任意两条并发邮件:一封投给 B,B 的插件回信进 C 的 handler,
而 A 的 `notify.Recipients` 还没跑完。
③ heartbeat 那条 goroutine 每 10s 写一次(见 heartbeatInterval
注释:实测本机 SSE 连接只活 34~57s,被中间反代按空闲超时掐掉),
撞车概率随在线时长线性上升。
## 修法
Client 加 `writeMu`,串行化**全部四条**写路径:
Send / SendWithID / heartbeat / replay
`replay` 虽在注册之前、按构造就是单写者,仍持锁 ——
让「对 Res 的写入一律经由 writeMu」成为**结构上**的纪律:
将来有人把注册提前或把回放挪到注册之后,没上锁的版本会静默退化成并发写。
为什么不能靠上层串行化:推送方有 5 个入口
(SendToUser/SendToAgent/SendToRecipient/Broadcast/replay),
要保证"同一 client 的所有写互斥",责任只能落在 client 自己身上。
## 判据(新增 frame_integrity_test.go,2 格)
**用帧完整性而不是"不许有 race"当判据** —— 本仓 `go test ./...`
默认不带 -race,判据必须在默认路径能判,否则就变成"要记得加 flag"。
TestFrameIntegrityUnderConcurrentPush 800 帧必须 800 帧完整
TestHeartbeatDoesNotInterleaveWithPush 钉住"只锁推送漏掉心跳"那个漏法
变异验证(去掉三处锁):
★ 17 次交错;切出 793/800 帧;心跳格也报 3 次交错 ⇒ 两格都有分辨力。
|
2026-09-28 08:26:29 +08:00 |
|
|
|
cbfc3bdde7
|
fix(安全): 构建产物出库 + pre-push 加**按内容**的第二道闸
两个 36MB 的 .hap 被 f51c9c8(一个标题为「workspace 谓词抽成共享构造器」的
重构提交)顺手带进版本库,而它们**内嵌 AGC 配置真身**。
## 实测确认(不是推测)
用 python zipfile 打开两个 hap,逐字段核对形状(不打印值):
resources/rawfile/agconnect-services.json
client.client_id HEX len=19
code.code1..code4 HEX len=32 ×4 ← api_key 信封
oauth_client.client_id HEX len=19
app_info.app_id HEX len=19
`git merge-base --is-ancestor f51c9c8 origin-https/main` 判否
⇒ **仅本地、尚未推送**(本仓镜像是 public / 匿名可 clone)。
## 为什么只 rm --cached 不够
`git rm --cached` 只把文件移出 index,**blob 仍躺在未推送的提交里** ——
下一次 `git push` 会连它一起发出去。所以必须同时堵"下一次"。
## 改法
① .gitignore 加 `*.hap` / `*.app` / `*.ipa`
② .githooks/pre-push 加第二道闸:**按内容**查,不按路径
—— 原先只按路径拦 agconnect-services.json,而 hap 里它只是
一个 zip 条目,逐条列举路径追不上产物形态。
判据形态:本次推送范围内**新增或修改**的构建产物(`*.hap/*.app/*.ipa`)
→ 内容里出现 `agconnect-services.json` 这个**条目名** ⇒ 拦。
用条目名而非凭证值:值会变而条目名稳定,且搜值需把凭证读进内存。
## ★ 这一格踩了三个坑,每个都让闸**静默放行**(都实测过)
① `git diff "a..b"` 取不到"该范围新增的文件" —— 它的语义是
「工作区 vs b」,**干净工作区上恒为空**。第一版这么写,
实测含真凭证的 hap 被放行、push exit=0。
⇒ 改 `git log --diff-filter=AM --name-only`(天生吃范围)。
② `git rev-parse "a..b:path"` 输出**两行**(blob sha + `^a`),
赋进变量是多行值 ⇒ 后续 `cat-file` 失败 ⇒ 被 `|| blob_sha=""`
兼掉 ⇒ 静默跳过。
`git ls-tree "a..b"` 也报错(不接受范围)。
⇒ 正确形状:rev-list 取 tip → ls-tree <tip> 取 blob。
③ ★★ `grep -q` 接管道 + `set -o pipefail`:grep 命中即退出 ⇒ 上游
`git cat-file` 收 SIGPIPE 退 **141** ⇒ pipefail 取各段合取 ⇒
整条管道 141 ⇒ `if` 判假。**"找到凭证"被读成"没找到"。**
实测 PIPESTATUS=141 0。
失效方向恰好是**放行**,与本钩子"宁可推不出去"相反。
⇒ 去掉 `-q`,让 grep 读完整条流(最后一段自然是它的码)。
## 验证(四态,都实测过)
含真凭证的 hap ⇒ 拦,裸仓没收到 commit
干净的 hap ⇒ 放行(只有 example.json)
AGC json 路径 ⇒ 仍拦(第一道没坏)
删分支 ⇒ 不误拦
commit-hygiene ⇒ 绿
|
2026-09-28 08:26:15 +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 |
|
|
|
db640e2360
|
fix(repo): platform_sessions 整表替换的域是 (agent, workspace) 而非 agent
修 `DEBTS.json` 里记的 `platform-mirror-replace-domain-too-wide`
(pi `b9c7308c` 报的,当时只做了定位未修)。
# 缺陷
`ReplacePlatformSessions` 的 DELETE 域是 `agent_name` 单列,而**每个上报者
只知道自己一个 directory**:
plugins/opencode-mail-bridge/index.js:1147
client.session.list({ query: directory ? {directory} : undefined })
⇒ A 工作区的桥上报一次就把 B 工作区上报过的镜像全擦掉,下个工作区的桥
再上报又擦掉 A 的。表现为「镜像按 project 轮换」。
# 生产实测(不是推断)
sqlite3 agent_platform_sessions GROUP BY workspace:
dsh 77 条散在 **25** 个工作区(/home/program/agentmail 25、/tmp 20 …)
pi 151 条散在 **62** 个工作区
# 后果已在生产数据上可见
镜像被擦 ⇒ `notify/mail.go` 的 `PlatformSessionFor` 查不到 ⇒
`sessions.platform_id` 留空。实测 **18 条活跃会话里 17 条 `platform_id` 为空**。
空 platform_id 不止"少个跳转":`notify/mail.go:94` 用它决定
`platform_session_id` 发给谁,owner 取错就抛「平台侧会话已删」⇒ 邮件静默消失。
# 修法
DELETE 域收窄到**本次上报覆盖的那些工作区**(wsOrder,去重保序)。
一次上报跨多个工作区 ⇒ 那些各自整表替换;本次没出现的一律不动。
仍然是"整表替换"而非增量合并 —— 镜像是平台快照,增量合并会让已删会话永远
留在候选里,而 session 位是三态语义、指向不存在的会话直接 404("选了却送不到")。
## ★ 一条判据覆盖不到的分支,单独补了判据
`if len(wsOrder) > 0` 这个守卫(wsOrder 为空 ⇒ 什么都不删)**既有判据碰不到**:
所有既有用例传进来的 list 都带 workspace。实测把守卫改成 `>= 0`(空清单也按
agent 清,退回缺陷),**全部既有判据仍然绿**。
补 `TestReplacePlatformSessionsWithNoWorkspaceKeepsEverything`:
一次不带 workspace 的上报后,`/A` 与 `/B` 的镜像都必须还在。
变异验证:该判据能抓住这个变异(而既有判据抓不住)。
# 关于"空 IN ()"
守卫去掉会拼出 `workspace IN ()`。SQLite 与 PostgreSQL **都**是恒假(不报错),
所以行为上等价 —— 但那是依赖两个数据库的隐式巧合,不是读代码能看出来的保证。
守卫保留,并在注释里写明这一点。
# 生产验证
部署后用 opencode 的真 key 打一次带 `workspace=/ZZZ` 的心跳:
· 写入 opencode /ZZZ 1 行
· **dsh 的 25 条 /home/program/agentmail 镜像一行没少** ✓
(修前这次上报会把它们全擦掉。已 DELETE 掉测试行)
注:三个桥本次心跳都没带 `platform_sessions`(opencode 的 `reportSessions`
在 `directory` 为空且拉取失败时返回 `undefined`,服务端按 nil 跳过替换),
所以"三次采样镜像不变"**不能**作为修复生效的证据 —— 上面那次主动打心跳才是。
# 未解决(DEBTS 那条的后半)
`agent_platform_sessions` 主键仍是 `(agent_name, platform_id)`:
同一个 platform_id 出现在两个 workspace 会撞 UNIQUE ⇒ 无 ON CONFLICT +
defer Rollback ⇒ 整个 DELETE 回滚 ⇒ 镜像永久停滞。
本改动只消除"擦错别人",没消除"同 id 跨 ws 撞约束"。要不要给 PK 加 workspace
仍未决(涉及 SQLite 需重建表 + 具名索引会丢 + 孤儿 _new 表自愈,见 DEBTS 原文)。
|
2026-09-26 14:20:23 +08:00 |
|
|
|
667d368a48
|
refactor(repo): workspace 谓词抽成共享构造器 + 删一个死函数
用户 2026-09-26:「审查一下服务端,我觉得现在还是有大量不符合设计的地方与冗余代码」。
# 先说审查结论:**"大量冗余"核不出来**
| 检查项 | 读数 |
| --- | --- |
| 99 个 handler | **全部注册,零死路由** |
| 死函数 | 2 个(本次删 1,另 1 个被测试用、保留) |
| 注释占比 | 23%(这个仓每个非显然决定都记"为什么",是有意的) |
| 测试 | 13644 行 = 源的 41% |
# 但找到一处真问题:`workspace` 谓词手抄了三遍
同一件事在三处各写一遍:
args := []any{agentName}
if strings.TrimSpace(workspace) != "" {
args = append(args, workspace)
q += fmt.Sprintf(` AND s.workspace = $%d`, len(args))
}
★ 代价不是"多几行",是**加参数要改三处、漏一处不会编译报错**。
本次给三个函数加 workspace 参数(`ListInboxScoped`/`CountUnreadScoped`/
`MarkAllInboxReadForSession`)就是手抄了三遍。
同仓有同类先例:`quota.go` 里那条 `★★★ 判据自检` 记的
「占位符编号错位导致静默少行」—— 根因完全一样(同一个模板抄多处,
靠人肉保持一致)。
⇒ 抽 `workspaceScope(q, args, workspace) (string, []any)`,三处各变成一行。
# 为什么"必需"这条不在 repo 层
`checkWorkspace` **允许空**:空 = 不过滤 = 人类侧(一个人跨工作区,WebUI
按 session_workspace 分组显示)。"Agent 侧必须带"是**接口契约**,放在 Handler。
抽出来的函数注释里把这层分工写死了,免得后来者以为 repo 层该拒绝空值。
# 与 `FindOrCreateDefaultSession` 里那套**故意不共用**
那里要的是「工作区为空时从 mails 反推」(历史会话兼容),语义更宽。
合并前要先确认那是不是想要的行为 —— 现在保持分开。
# 删 `SessionMailCount`
全仓零调用(连测试都没有)。`GetSessionMailByID` 也只被两个测试用,
但它是那两个测试的被测对象,**不删**(测试专用包装与死代码不是一回事)。
# 验证
· 变异:把 `workspaceScope` 改成永远不过滤 ⇒
`TestInboxListIsScopedByWorkspace` + `TestMarkAllReadIsScopedByWorkspace` 判红
· 12 个包通过;`internal/repo` 唯一的 FAIL
(`TestReplacePlatformSessionsKeepsOtherWorkspaces`)**改动前就红** ——
已用 `git stash` 式回退验证,它是 `DEBTS.json` 里记的 platform_sessions
PK 缺陷那条判据,与本次无关。
# 顺带记一笔(对我自己的)
本机 `go` 是 1.24.4 而 `go.mod` 要求 1.25.0,**`go build` 会去下载 toolchain
并因离线失败**(exit=1)。我前面几轮用 `go build ./... | head -5 && echo "编译 ok"`
判断,把 `head` 的 exit 0 当成了编译成功 —— **那是假的**。本轮才发现,
改用本地已有的 `toolchain@v0.0.1-go1.26.7` 才拿到可信结果。
⇒ 判据里凡用 `cmd | head && echo ok` 的形状,退出码被管道最后一道吞掉,
之后一律用 `cmd >/dev/null 2>&1; echo $?` 或显式检查 `${PIPESTATUS[0]}`。
|
2026-09-26 14:08:48 +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 |
|
|
|
4175c0ba45
|
fix(bridge): ★ 投递即标已读 —— 修「桥重启 → 重投 → 回声」
用户 2026-09-26 原话:
「我都不记得我下达这个任务,是你的桥自动重投存在 bug」
「就是你的错误的重投机制造成了回声」
# 我上一轮把因果搞反了
我先认定是「两个 Agent 自发辩论」,还为此写了第三道防线(数 Agent↔Agent
连续往返)。**方向错了** —— 是**桥把同一封信反复投递**,每次投递起一个
worker 回信,回信又触发下一轮。模型在做什么?它在回答一封被重复投进来的
旧信。用户根本不知道有这回事。
# 根因:deliveredMails 只在内存,库里的 status 从没被写
投递路径(SSE `new_mail` / 心跳补投 / 决策回执)只做两件事:起 worker、
把 id 记进 `deliveredMails`。**没有任何一处调 `/mail/read`** —— 桥里唯一
那处标已读在 `read_inbox` 工具里,要等模型自己去读收件箱。
于是每封被投递的信**永远是 unread**;而 `catchUp` 按 `status=unread` 拉
⇒ 桥一重启(**每次部署都会**),积压的"未读"被当成离线漏投**再投一遍**。
# 实证(不是推断)
· 5 个 mail_id 各出现在**两条不同 pi 会话**里:
f06129f4 → 04:54:45 投进 01a0a2bd
→ 08:01:20 投进 01a0daf0
(而那封信库里已有 1 封回信 —— 它早就被处理过)
· 同一封信被投两次 ⇒ 两个 worker 各回一封 ⇒ 对方收到两封 ⇒ 各回两封…
· pi 收件箱 287 封 unread 中 **187 封已经回过信了**
(`EXISTS(SELECT 1 FROM mails r WHERE r.parent_mail_id=m.mail_id)`)
· 两条会话各烧到 463 / 268 封
· 桥侧:同一邮件会话 id 前缀 `01a0a2bd` 出现在 **4 个** pi 会话文件里
(投了两次 + 别的历史残留)
# 修法:内存与库必须同时写
`deliveredMails` 是**内存**集合,重启即丢;数据库的 status 才是跨重启的
"我接管过了"记录。两者只写其一 ⇒ 口径不一致 ⇒ 重投。
新增 `markDelivered(id)`:**凡是标记"我接管了这封"的地方都走它**
(SSE / 补投 / 决策回执三个投递点),同时写内存与库。漏一处就是一条重投
路径 —— 这正是缺陷的形状(四处各自 add,没有一处标已读)。
标已读只改 status,不改内容、不删行;`read_inbox` 传 `status=all` 照常可见。
而"已交给一个 worker 处理"正是那封信此刻的真实状态 —— 库里本来就该记这件事,
而不是"模型有没有顺手调过 read_inbox"。
# 四个桥:三个有缺陷,第四个早已修过
| 桥 | 投递标已读 | 说明 |
| --- | --- | --- |
| pi | ✗ → ✓ | 三处 add 都不标 |
| opencode | ✗ → ✓ | 同上 |
| dsh | ✗ → ✓ | 同上 |
| **homeagent** | **✓ 早有** | `ledger` 落盘,跨进程 |
homeagent 不用这个修法:它的 `ledger` 记 `delivered`/`completed` 两个状态,
只有 `completed` 才跳过(投过但被中断的**仍然重投**并带说明)—— 那份设计的
注释里就写着 18:59:38 那次实测,比我今天这个修法更早也更完整。
所以对它只做了「补投按工作区收窄」那一半(见下条)。
# 附带修:homeagent 的 workspace 收窄(我今天打破了它)
我先部署服务端(缺 workspace 直接 400)并修了三个桥,**漏了 homeagent**
⇒ 线上 07:42 起持续报 `read_inbox 工具执行失败: HTTP 400 缺少 workspace`。
这是我造成的,靠自己的日志发现的(pid 还是重启前的旧进程 2291455)。
修法与另三个同源:`currentWorkspace`(信封的 `to_workspace`)在回合期间暂存
(与 `currentSessionID` 同一形状、同一生命周期),`inboxURL`/`scopeQuery` 带上它,
补投从"读一次全局收件箱"改为逐工作区(清单来自心跳的 `pending_workspaces`)。
# 清理重投燃料
151 封归档(80 封回声:会话全程无人类 + 71 封 `permission_decision` 不可投)。
★ 用 `archived` 而不是 `read` —— `read` 还能被 `status=all` 拉出来重投。
判据用服务端自己的口径(`unreadFor` = `m.status<>'archived'` 且
`mail_reads` 无该读者),不手写 SQL 猜语义。
后置:pi / dsh / opencode / homeagent 在**所有工作区**的 unread 全部为 0。
# 判据
· `delivery-marks-read.test.mjs` × 3(pi / opencode / dsh)各 4 条:
核心那条钉的是「`deliveredMails.add` **只允许**出现在 markDelivered 内部」——
任何别处直接 add 就是绕过标已读的重投路径。另加自检反例。
变异验证:绕过投递点 / markDelivered 不写库 / 补投绕过,三处全判红。
· `inbox_workspace_test.go`(homeagent)5 条:URL 带 workspace、带不到时不带
(让服务端 400:错误可见好过静默越界)、补投逐工作区、两处投递路径都设工作区
且都清空。变异 3 处全判红。
· 改了两条既有判据(pi / dsh 的 permission-note):原来钉
`deliveredMails.add(decisionMailID)` —— 那个形状**就是**缺陷载体。
语义没变(仍"不再当新任务"),载体变了。
全量:pi 517 / opencode 344 / dsh 407 / homeagent 除一条既有的
`TestSDKPinMatchesBuildMachinePointer`(依赖构建机路径,改动前后同样红)全绿。
|
2026-09-26 09:18:39 +08:00 |
|
|
|
239ff37291
|
docs: 回 pi e77154d1 —— 撤回 T\L 探测器形状(修后必假阳);(d) 拆 d1/d2,d1 已落地为真判据
① §二 收: T\L 的根因错在"即将被销毁"由 DELETE 谓词决定,不由 T\L 决定
修前 DELETE 域=整个 agent ⊋ L ⇒ 碰巧对;修后 DELETE 域=按 ws 删=L ⇒ 恒假阳
修好后表里天然共存多 ws ⇒ 每次心跳常鸣 ⇒ 落进「还清了反而红」那个坑
⇒ 我为 (d) 拒绝超前断言的理由,在我自己的形状里以假阳形式复现了
⇒ 忠实形状: destroyed = 被本次 DELETE 移除且未被本次 list 重插的 ws(与实际删除域同源)
② §三 收: d1 三条性质(今日可写/现在红/修好即绿)逐条成立 ⇒ 该现在就建,不该进 due
⇒ 我整体归入 due 是「超前断言」的**反面错**(把今天能给的判据当成要等未来)
⇒ 已建 TestReplacePlatformSessionsKeepsOtherWorkspaces(失败信息列出存活 workspace 及行数)
⇒ 登记 platform-mirror-d1-cross-workspace(28→29);d2 与 scope 字段同 due
③ ★ 自查: 探针第一版把「取 T」写在 Replace 之后 ⇒ 量到删除后的表 ⇒ 结论会全反
⇒ 修正后修前 T\L=[/A] 响 ✓。教训: 观测点必须与被观测的判据在同一时刻
④ 测试: ./internal/repo/ 245 通过、唯一红项即 d1;探针已删
⑤ 工作树另有别的 agent 在飞改动,未触碰;提交按显式路径只取我的文件
|
2026-09-26 09:15:44 +08:00 |
|
|
|
9311612358
|
test(repo): 建 (d1) 判据 —— 上报非空 list 时不得删除其它 workspace 的行(**今天可写、现在红、修好即绿**)
pi `e77154d1` §三 指出我"把 (d) 整体归入 due"是**反方向的错**: 判据的**可得性**本身要复核 ——
把今天就能给的判据当成"要等未来才能给",余额里就挂着一个今天就能变绿的缺口。
我先写仓内探针逐条跑(跑完即删),四条读数:
[修前] 播下 /A=2 → B 上报 /B=1 ⇒ (d1) FAIL: /A = 0,期望 2 ★本缺陷
[修前] T\L @DELETE前 = [/A] ⇒ 响 ✓(我那形状确实抓得到真缺陷)
[修好] 共存 /A=2 /B=2 → /A 仍 = 2 ⇒ (d1) PASS ⇒ 修好即绿 ✓
[修好] T\L @DELETE前 = [/A] ⇒ ★ 假阳:修好后每次心跳都常鸣
[修好] 与"实际删除域"比 destroyed = [] ⇒ 不响 ✓ 无假阳
⇒ ★★ 同时**撤回我 §三 提的 `T\L ⇒ WARN` 形状**(记入 DEBTS 补记之九):
根因: "即将被销毁"由 **DELETE 的谓词**决定,不是由 T\L 决定。
修前 DELETE 域 = 整个 agent ⊋ L ⇒ T\L 恰等于被销毁集合(碰巧对)
修后 DELETE 域 = 按 ws 删 = L ⇒ 被销毁 = ∅,而 T\L 仍非空 ⇒ **恒假阳**
而修好后表里天然共存多 ws(那正是修复目标)⇒ **每次心跳常鸣**
⇒ 落进本仓「**还清了反而红**」那个坑 —— 我为 (d) 拒绝超前断言的理由,
在我自己提的形状里以假阳形式复现了。忠实形状: `destroyed = 被本次 DELETE 移除
且未被本次 list 重插的 ws`(与实际删除域同源 ⇒ 修前响/修后不响,且 [] 时仍覆盖)
⇒ ★★ (d) 拆两半: **d1 = 本条**(每项自带 Workspace ⇒ 不需要请求级字段 ⇒ 今日可判);
**d2 = 上报 [] 时只清自己那个 ws**(需要"这次上报属于谁")⇒ 与 scope 字段同 due
自查: 探针第一版把"取 T"写在 Replace **之后** ⇒ 量到删除后的表 ⇒ 结论会全反
(据此差点得出"漏报"的相反结论);已把取 T 排到 B 上报**之前**重测。
教训: **观测点必须与被观测的判据在同一时刻** —— 与"判据要锚定到它防的那个动作"同一条。
测试: ./internal/repo/ 245 通过、唯一红项即本条(它断言的正是尚未修复的缺陷)
登记: platform-mirror-d1-cross-workspace(余额 28→29);-run Debt ⇒ ok
|
2026-09-26 09:15:30 +08:00 |
|