现象(实测,可复现):
agent_platform_sessions 在 10 个状态间轮换,每次差的恒为一个 project 的会话数
(/tmp=49 /root=38 am-mcp-probe=23 agentmail=37 TrueAgent=100 llmsproxy=18
facemodule=7 Liquid=7 NextAgent=2 空),同状态内 reported_at span = 0.0ms
⇒ 每次都是**整表替换**,而每个上报者只知道一个 directory
⇒ 后一个把前一个的清单整体擦掉
危害(口径已修正):
候选 = 来源1(本侧 sessions, 实测 6) + 来源2(该镜像, 实测 37)
镜像被擦时该 workspace 的候选 ~43 → ~6(掉 37 条),**不是归零**
(先前写 37→0 是漏了来源1)
★ 为什么记的是「到期前提」而不是「修法」:
它是**带顺序约束**的: 若只给 DELETE 加 workspace 而 PK 不动,则
「同一 platform_id 出现在两个 workspace」⇒ UNIQUE constraint failed (1555)
⇒ INSERT 无 ON CONFLICT(grep=0) + defer Rollback ⇒ **整个 DELETE 回滚**
⇒ agents.go:186 降级 -1、桥不读该字段(grep=0) ⇒ 三重静默
⇒ 从「间歇擦除」变成「永不自愈的静默停滞」(更难查)
故到期前提写死为: **先 PK 加 workspace,再 DELETE 加 workspace**
三个已被推翻的根因(留作反面材料,见 note):
① 「读域 vs 擦除域,且 [] 是 truthy」——非主因(80% 是非空替换)
② 「擦除在语义上不必要」——错,整表替换有意且有 TestReplacePlatformSessionsIsFullReplace
③ 「今天不撞靠 session.id 全局唯一」——因给错了,实测同 id 换 workspace 也不撞,
真正原因是「全量 DELETE + seen 去重」两处代码结构
校验: go test ./internal/repo/ -run Debt ⇒ PASS(余额 27,新条已计入)
208 lines
43 KiB
JSON
208 lines
43 KiB
JSON
{
|
||
"_": [
|
||
"欠账的**单一登记**(pi 2026-09-14 裁定 §3):三笔类型不同、但必须能一眼看全。",
|
||
"为什么要一个文件:三笔原先各自表达(RESULT static=5 / t.Skip / 登记在文档里的到期前提),",
|
||
"没有一处能看全 —— 而『欠账不显形,就等于没有』;分散在多处的登记,审计时只会被找到一处就当全部。",
|
||
"两端都读这个文件:Go 侧判据断言自己的条目与**实测**一致(不许留一份手写的数字),",
|
||
"electron 套件把它打进 RESULT 行(那是常态可见的位置)。",
|
||
"已结算(2026-09-14):calendar-today-recompute —— P6 第 1 步(日历页 pages/CalendarPage.ets)落地,today 走 `@Prop @Watch('onVisibleChanged') visible` 在 pane 变可见时重算(另有 aboutToAppear 覆盖重新挂载),判据 test/harmony-calendar.test.mjs 的「★ today 在 pane **变可见时**重算」。结算即从此清单移除,余额里不再计这一笔。",
|
||
"已结算(2026-09-19):gesture-semantics —— P6 第 3 步(左右滑动翻页)落地:鸿蒙侧 `CalendarPage.ets` 加了 `PanGesture`、判定逻辑在纯逻辑层 `model/Calendar.ts` 的 `judgeSwipe`。按该条自己的口径(「鸿蒙侧出现滑动手势代码时**立即建**」)同步建了语义契约判据 `test/cross-client-gesture.test.mjs`(8 条)—— 按 (b) 口径钉**语义**不钉数值:左滑=下一段/右滑=上一段/纵向优先/快滑窗口/手势与按钮共用同一翻页函数/无边界回弹/有意差异被记录/方向写反必红。另在 `harmony-logic.test.mjs` 加了行为判据(跑 `judgeSwipe` 的四道门)。结算即从此清单移除,余额里不再计这一笔。",
|
||
"2026-09-19 新增 wide-breakpoint-divergence:两端断点不同且含义不同(审计发现,此前无记录)。它不是\"已确认的缺陷\",而是**未决的口径** —— 登记它是为了让「两者不同」这个事实本身可见,而不是让它藏在两处代码里。",
|
||
"2026-09-19 新增 mail-list-attachment-count:邮件列表的附件数两端都拿不到(WebUI 那段是死代码)。鸿蒙侧只补了有真数据的「抄送 N」。",
|
||
"2026-09-19 新增 harmony-maildetail-missing-three:邮件详情页缺三块功能(改名建议 / 转发 / 预算编辑),服务端都已支持。"
|
||
],
|
||
"debts": [
|
||
{
|
||
"id": "static-criteria",
|
||
"count": 5,
|
||
"due": "本工作区能装、能点设备(探针三值转 true 时自动变红)",
|
||
"where": "client/electron/test/run-all.mjs 的 STATIC_ONLY(2026-09-19 起 harmony-admin 那条已升级为设备判据、移出名单 ⇒ 6 → 5:test/harmony-appearance.test.mjs、test/harmony-logic.test.mjs、test/cross-client-theme.test.mjs、test/appearance-defaults.test.mjs、test/harmony-imageprep.test.mjs)",
|
||
"kind": "scope",
|
||
"note": "2026-09-19:`harmony-admin` 的条目**已升级**(管理页真的能打开、列表真的渲染出用户行)并移出 STATIC_ONLY ⇒ 本笔 6 → 5。剩下的五次升级按\"每条缺什么设备侧验证\"逐条来,不为了把数字消成 0 而凑 —— 凑出来的设备判据只是把\"没验\"换成\"假装验了\"。\n\n2026-09-19 再更新:`cross-client-theme` **升级了一半** —— 新增一条设备判据(读悬浮球像素:品牌色真的画成 `#2563EB`;且球上图标与底色 WCAG 对比度 ≥3:1),并顺手钉了一条静态防线(`Theme.surface` 不得当代的前景色)。**它仍留在 STATIC_ONLY 名单里**,因为 `.ets` 那半只有悬浮球这一处上了设备 —— 其余令牌仍是静态对齐。「一半」要写出来,不能让名单看起来像没动过。"
|
||
},
|
||
{
|
||
"id": "mails-status-derived",
|
||
"count": 1,
|
||
"due": "详情/线程改为按读者派生(readStateFor)之后 —— 那时 mail_status_derived_test.go 从 Skip 转实跑",
|
||
"where": "server/internal/repo/mail_status_derived_test.go",
|
||
"kind": "scope"
|
||
},
|
||
{
|
||
"id": "observability-output",
|
||
"count": 1,
|
||
"due": "页面层(MainPage.ets)接上「读 presetSubstitutedFrom 并打一行日志」时;那一步同时补判据『读侧恰好出现 1 次且在日志调用里』",
|
||
"where": "尚无判据 —— 这正是欠账的一部分(P6 第 1、2 步动 MainPage.ets 时一起做);形态判据在位:test/harmony-appearance.test.mjs(bgBlur 消费侧计数)",
|
||
"kind": "scope"
|
||
},
|
||
{
|
||
"id": "unknown-preset-approval",
|
||
"count": 1,
|
||
"due": "有人对上表那格**追认或驳回**「未知 id 显示 aurora 而不是空白」这个方向时(我作为实现者不能自己追认自己)",
|
||
"where": "client/electron/test/CRITERIA.md §10 的『未知的预设 id』行(现为『无人类批准』)",
|
||
"kind": "env"
|
||
},
|
||
{
|
||
"id": "overlay-follows-app-theme",
|
||
"count": 1,
|
||
"due": "上设备后**翻转一次 colorMode**(应用深色 / 系统浅色),断言**解析出的遮罩值跟着「应用」主题变、而不是跟「系统」**;真机若证伪,正确修法是「遮罩从应用主题派生」,不是回到双常量",
|
||
"where": "client/harmony/entry/src/main/ets/common/Theme.ets:58-79 的注释(机制依据:AppearanceStore.applyTheme → app.setColorMode)——**注释不是判据,所以进余额**",
|
||
"kind": "env"
|
||
},
|
||
{
|
||
"id": "nav-dark-route-b-unguarded",
|
||
"count": 0,
|
||
"due": "已完成 2026-09-23(见 note)",
|
||
"where": "client/electron/test/background.test.mjs —— **已堵**:见 note。(原文:`.dark .nav-rail{}` 选择器作用域与组件 `dark:` 变体,变异确认过都会逃掉)",
|
||
"kind": "scope",
|
||
"note": "**已堵(2026-09-23)**。这三条逃逸路(A 选择器作用域 / B Tailwind `dark:bg-*` 变体 / C 元素级 `backdrop-blur-*`)此前各自无判据,变异确认过全逃得掉。到期条件(深色主题落地)在 2026-09-17 就成立了 —— 但那次只修了令牌这条路,欠债逾期至此。\n\n堵法(欠债原文点名的**文件窄豁免**,不是一刀切):`background.test.mjs` 新增三条 check,**只扫 `className` 里出现 `nav-rail`/`nav-item` 的那些类串** —— 问的不是「这个文件有没有 dark: 背景」,而是「挂在导航元素上的那个类串里有没有」(与逃逸路的形状同构;同文件别的元素写什么都不影响,避免 `bg-chrome-600` 那个先例的误红)。\nA 条另起一条:CSS 里不许出现 `.dark .nav-rail`/`.dark .nav-item` 这类选择器。\n三条都做了变异验证(逐条注入 ⇒ 各红一条;恢复 ⇒ 47/47 全绿)。"
|
||
},
|
||
{
|
||
"id": "nav-blur-route-c-unguarded",
|
||
"count": 0,
|
||
"due": "已完成 2026-09-23(见 note)",
|
||
"where": "client/electron/test/background.test.mjs —— **已堵**:见 note。(原文:元素级 `backdrop-blur-lg` 不在那条选择器下,变异确认过逃得掉)",
|
||
"kind": "scope",
|
||
"note": "**已堵(2026-09-23)**。这三条逃逸路(A 选择器作用域 / B Tailwind `dark:bg-*` 变体 / C 元素级 `backdrop-blur-*`)此前各自无判据,变异确认过全逃得掉。到期条件(深色主题落地)在 2026-09-17 就成立了 —— 但那次只修了令牌这条路,欠债逾期至此。\n\n堵法(欠债原文点名的**文件窄豁免**,不是一刀切):`background.test.mjs` 新增三条 check,**只扫 `className` 里出现 `nav-rail`/`nav-item` 的那些类串** —— 问的不是「这个文件有没有 dark: 背景」,而是「挂在导航元素上的那个类串里有没有」(与逃逸路的形状同构;同文件别的元素写什么都不影响,避免 `bg-chrome-600` 那个先例的误红)。\nA 条另起一条:CSS 里不许出现 `.dark .nav-rail`/`.dark .nav-item` 这类选择器。\n三条都做了变异验证(逐条注入 ⇒ 各红一条;恢复 ⇒ 47/47 全绿)。"
|
||
},
|
||
{
|
||
"id": "boundary-vocabulary-incomplete",
|
||
"count": 1,
|
||
"due": "**由外部读者报告时**(自查机制对这一类结构性失明 —— 发现词表外说法的机制,正是看不见它的那个机制)。收到报告后:扩词表 + 登记该处 + 保留\"上一次是谁发现的\"。**没有内部触发器,这是这条递归的不动点**:无论词表多长、判据多严,总有一类盲区只能靠\"外面有人读了一遍\"",
|
||
"where": "client/electron/test/debt-visibility.test.mjs(词表键控的盲区:**已知未覆盖**——词表是采样、不是完备)",
|
||
"kind": "env"
|
||
},
|
||
{
|
||
"id": "redeploy-script-unguarded-steps",
|
||
"count": 0,
|
||
"kind": "scope",
|
||
"due": "**已还清 2026-09-25**(见 note)",
|
||
"where": "`deploy/redeploy-gateway.sh` 的四类副作用步骤 —— **已堵**(见 note)。(原文:`deploy/redeploy-gateway.sh:84` 的 `run \"cp -r …\"`,`run()` 内 `eval` 的失败既不中断也不被调用点接收)",
|
||
"note": "**已堵(2026-09-25)**,出口是这条欠账自己写的到期条件(\"下一次改 `deploy/` 下任一脚本时\")—— 当天因修 `--dry-run` 落地写入而动了该脚本,故一并还。\n\n堵法按原文给的形状(`|| { bad …; exit 2; }`,与既有 2=环境/1=检查 对齐),四处:\n ① 前端同步三步(`rm -rf assets` / `rm -f index.html` / `cp -r dist`)各自接收退出码;\n `cp` 尤其要紧:没拷进去 ⇒ go:embed 把**旧界面**打进二进制,而单测仍全绿(2026-09-14 踩过)。\n ② `systemctl stop` 加守卫 —— 这是本条目原文点名的\"stop 失败而状态没人看\"。\n 加它的理由比原文更强一层:`systemctl start` 对**已在运行**的服务是 **no-op** ⇒ stop 没成功时后面那句 start 什么也不做,**旧进程继续跑旧代码**,而脚本一路走到后置验证报成功 —— 即\"部署脚本跑过了 ≠ 线上跑的是当前代码\"被脚本**自己在内部**造出来。\n ③ am-sandbox 段的 `go build`/`install` 并入 DRY_RUN 分支。\n ④ `install -d $PREFIX/bin` 与通知脚本 `install` 并入 DRY_RUN 分支(同批修的 `--dry-run` 会落地问题)。\n\n**未一并做的**(如实记,避免本条读起来像\"全脚本已无裸步骤\"):`redeploy-plugin.sh` / `install.sh` 的同类步骤仍未逐个加守卫 —— 那是另一条欠账 `deploy-interrupt-trap-other-scripts` 的范围,本条只覆盖 `redeploy-gateway.sh`。"
|
||
},
|
||
{
|
||
"id": "deploy-space-prefix-fs",
|
||
"count": 1,
|
||
"kind": "判据铺得不满(不是新列)",
|
||
"due": "下一次因空间问题失败时;或有人愿意补一行 df 时",
|
||
"where": "deploy/lib/env-defaults.sh ②b 只判了 $TMPDIR,没判 $PREFIX 所在的文件系统"
|
||
},
|
||
{
|
||
"id": "pi-bridge-adopt-cwd-mismatch",
|
||
"count": 1,
|
||
"kind": "沙箱 rw 与 worker 实际 cwd 的第三个来源未对齐(同类已修两处,剩接管路径)",
|
||
"due": "下一次动 pi 桥的会话装载 / `session-scan.mjs` 时;或有人愿意把「这一轮用哪个 cwd」完全收成父进程一处决定时",
|
||
"where": "`plugins/pi-mail-bridge/src/worker.mjs` 的**接管会话**分支:worker 用会话文件 header 里的 `info.cwd`(`resolveWorkspaceCwd(info.cwd || data.to_workspace, …)`),而父进程(`src/pool.mjs` → `src/turn-cwd.mjs`)只能从 `state` 里拿 `{sessionFile, cwd}`,**读不到 header** ⇒ 接管的首回合 rw 仍可能不含 worker 真正要写的目录。父进程要拿 header 得用 `src/session-scan.mjs`,但 `readHeader` 未导出、整表 `scan()` 在父进程里代价大(worker 里实测 1431ms / 堆瞬时 240MB,见 worker.mjs 里那段注释)。★ 这类错位的**特征是没有提示**:EACCES 落在「界内」,读日志的人会以为沙箱装错了(不像 `ask` 还有一次问)。修法方向:把「这次用哪个 cwd」收成父进程一处决定(它已有 `state.sessionFile`,header 也可读),worker 只消费、不再自己推导 —— 即 `src/turn-cwd.mjs` 头注释里写的「三来源变一来源」。"
|
||
},
|
||
{
|
||
"id": "deploy-interrupt-trap-other-scripts",
|
||
"count": 1,
|
||
"kind": "只在 redeploy-gateway.sh 做了,另两个部署脚本没做",
|
||
"due": "下一次动 redeploy-plugin.sh / install.sh 时",
|
||
"where": "只有 redeploy-gateway.sh 有 INT/TERM/HUP trap;install.sh 与 redeploy-plugin.sh 在写系统目录期间被打断同样会留半成品(它们没有\"服务停着\"那种后果,所以优先级低)"
|
||
},
|
||
{
|
||
"id": "harmony-p4c-boundary-decls",
|
||
"count": 4,
|
||
"due": "本工作区能装、能点设备 —— 那时这几条静态判据里被替代掉的那些断言换成真机断言,声明随之减少",
|
||
"where": "client/electron/test/harmony-admin.test.mjs、client/electron/test/harmony-imageprep.test.mjs",
|
||
"kind": "scope",
|
||
"note": "2026-09-19 更新(设备可用了,开始逐条升级):① `harmony-admin` 的那条**已升级为设备判据**(管理页真的能打开、列表真的渲染出用户行)⇒ 从本组减 1。② `harmony-imageprep` **只做了一半**,如实记着:新加的设备判据用真实素材(用户真上传的那张壁纸 1402×1122 / 152570 字节,从 GET /me/appearance/image 取回)验了「设备上读到的尺寸/体积与压缩决策的输入对得上」;**未做**「应用真的用 image.createImagePacker() 压一次、产出字节落在预期区间」——那需要一个**用户选图**入口(DocumentViewPicker,要人操作系统选择器),自动化里没有稳定路径。编一个绕过选择器直接调 packJpeg 的测试专用入口,会是只有测试在用的代码 —— 那种代码不会被真实场景触到,验它等于验一个不存在的东西。⇒ 剩 4 条里这一条要等一个**真人操作**的验证窗口(或平台提供可注入的选择器)。"
|
||
},
|
||
{
|
||
"id": "deviceprobe-fixture-timing",
|
||
"count": 1,
|
||
"due": "把两份 fixture 变成**当场采集**(跑 hdc dump 取现场)而不是人工存文件时,这条就到期",
|
||
"where": "client/electron/test/harmony-deviceprobe.test.mjs(两处提到「未验」的断言 + fixtures/aa-dump-l-*.txt)",
|
||
"kind": "scope"
|
||
},
|
||
{
|
||
"id": "wide-breakpoint-divergence",
|
||
"count": 1,
|
||
"due": "决定是否统一:鸿蒙 768vp vs WebUI 1024px(**不是 bug,是未决的口径**)",
|
||
"where": "client/harmony/entry/src/main/ets/pages/MainPage.ets(isWide 的 onAreaChange,>=768) 与 client/electron/src/hooks/useIsNarrow.ts(NARROW_QUERY = max-width: 1023px)",
|
||
"kind": "scope",
|
||
"note": "两端断点不同,且**含义也不同**,所以数值不同本身不算错:WebUI 的 1024 是「三栏(60 导航 + 320 列表 + >=520 详情 ≈ 900px,再加余量)放不下就退化单栏」,鸿蒙的 768 是「要不要显示侧栏」(鸿蒙没有并排的列表+详情三栏,它的内容区是一个窗格,所以 768 就够)。但**用户可见的后果**是:在 768–1023 宽(常见竖屏平板、窄窗口)下,WebUI 是单栏 + 底部导航,鸿蒙是侧栏 + 内容 —— 两台设备上同一宽度长得不一样。2026-09-19 由审计发现(此前**没有任何地方记录**这件事,连「两者不同」这个事实本身都没写下来,所以下一个人只会当成漏改)。待定:① 统一到 1024(鸿蒙跟 WebUI);② 统一到 768(WebUI 跟鸿蒙,但要重新论证三栏是否真能在 768 放下);③ 承认它们是两件事、把语义差异写进文档(那就该给两端各自的名字,而不是都叫 wide)。"
|
||
},
|
||
{
|
||
"id": "mail-list-attachment-count",
|
||
"count": 0,
|
||
"due": "已修(2026-09-20):字段确实返回,两端都可接",
|
||
"where": "server/internal/handler(邮件列表的响应构造) —— 客户端侧见 client/harmony/entry/src/main/ets/pages/MainPage.ets 的 MailItem",
|
||
"kind": "scope",
|
||
"note": "★★ 2026-09-20 **更正**:当初这条的结论是**错的**,根因是它把一个`omitempty` 造成的**字段缺失**当成了「服务端不返回这个字段」。\n\n原文(留档,别再犯):实测 `GET /me/mail/inbox` 的回包字段列表里「既没有 `attachments` 也没有 `has_attachments`」⇒ `MailList.tsx:261` 的 `mail.attachments?.length ?? 0` 恒为 0、那个 📎 在 WebUI 上从不出现(死代码)。\n\n事实:服务端 `GetInbox`(`mail.go:586`)**明确调了 `fillAttachments`**,并写了理由:「Agent 靠收件箱列表得知有哪些附件可下载,否则它不知道该调 attachment_id」。而 `Mail.Attachments` 的 json tag 带 **`omitempty`** —— **没有附件的邮件根本不输出这个 key**。\n我当初是**只看了一封没附件的邮件**就下了全称结论。\n\n2026-09-20 实测(`limit=200`,96 封):带 `attachments` 的 **2 封**,都是真有附件的那两封。⇒ **WebUI 那个 📎 不是死代码**,鸿蒙当初「有意不抄」的前提不成立。\n\n★ 教训:`omitempty` 字段的**缺失**不等于「服务端不返回」。判「某字段有没有」必须拿**确实有值的那条**去验,而不是拿一条恰好为空的数据。这与同一天那个真崩溃(`session_workspace` 的 `omitempty` → 客户端 `undefined` → 白屏)是**同一个坑的两面**。"
|
||
},
|
||
{
|
||
"id": "harmony-maildetail-missing-three",
|
||
"count": 0,
|
||
"due": "鸿蒙详情页对齐 WebUI 时(三块功能,服务端都已支持)",
|
||
"where": "client/harmony/entry/src/main/ets/pages/MailDetailPage.ets (对照 client/electron/src/components/MailView.tsx)",
|
||
"kind": "scope",
|
||
"note": "2026-09-19 审计发现:邮件详情页缺**三块 WebUI 有的功能**,而且**服务端三块都已支持**(不是做不了):① `RenameProposalBar`(MailView.tsx:324)—— Agent 建议改会话别名,人确认/驳回。服务端 `server/internal/handler/rename_proposal.go` 已在解析 `propose_alias` 标记并把提议从正文剥掉。**这条对寻址稳定性很重要**(WebUI 那里的注释:「Agent 干到一半自己改掉,人上一秒记住的地址下一秒就失效」)。② `ForwardBar`(MailView.tsx:378)—— 转发。服务端 `forward.go` 完整(含 `forwardSubject` 的 Fwd: 叠加处理)。③ `BudgetEditor`(MailView.tsx:235)—— 会话往返预算编辑(`max_rounds`,服务端 `forward.go:304` 有此字段)。三条都不是顺手能补的量级(各含交互 + 接口 + 状态),所以先登记,不在审计那一批里硬塞 —— 硬塞的结果是每条都半成品。\n\n2026-09-19 结算:三块**都做完了** ——\n① 转发(commit cc7ff25,`ForwardMailRequest` + 弹层;修了两个真 bug:两个动作球几乎完全重叠、弹层没高度导致键盘一弹按钮被顶出屏幕);\n② 会话改名建议(commit c26e857 接口 + fe4342a UI,端到端实测:服务端识别 rename_proposed → 建议条出现 → 点接受 → 库里别名真的改了);\n③ 往返预算(commit 4e84369,GET/PUT 契约用 curl 逐条实测)。\n★ 未验:预算条的**点击**在设备上没走通 —— 模拟器顶部 155px 是系统手势区,而折叠头部恰在其中(坐标式点击会被系统抢走)。数据链路已实测,观感没验。"
|
||
},
|
||
{
|
||
"id": "harmony-dead-pages",
|
||
"count": 2,
|
||
"due": "下次清理鸿蒙页面时(要先确认不是将来要用的留存实现)",
|
||
"where": "client/harmony/entry/src/main/ets/pages/InboxPage.ets(238 行)、pages/SessionsPage.ets(170 行)",
|
||
"kind": "scope",
|
||
"note": "2026-09-20 盘点发现的**不可达页面**(两页共 408 行):\n· `InboxPage.ets` —— 不在 `resources/base/profile/main_pages.json` 的页面表里,全仓**没有任何** `pushUrl('pages/InboxPage')`。\n· `SessionsPage.ets` —— 注册在页面表里,但**唯一的引用**是 `InboxPage.ets:171` 的 `pushUrl('pages/SessionsPage')`,而 InboxPage 本身不可达 ⇒ 一起不可达。\n\n★ 为什么没顺手删:`docs/HARMONY-ALIGN-PLAN.md:214` 把 `InboxPage.ets` 当作变异测试的靶子(往它塞一个色值来验证判据能判红),说明它在某个时点是有用的。删掉会永久丢代码,而它是否只是「早期实现的留存」我判断不了 —— **这是人的决定**。\n\n★ 它现在还带来实际成本:`cross-client-logic` 与 `harmony-arkts` 的**形状判据**会扫到它(并按「死代码」给它豁免)—— 每加一条形状判据都要为「这文件还活着」多写一次例外。要么删、要么在文件头明确写「这是留存参考、不参与构建」,两条路都比现在清楚。"
|
||
},
|
||
{
|
||
"id": "harmony-permission-history",
|
||
"count": 0,
|
||
"due": "**已完成 2026-09-21**(本轮「授权栏与 WebUI 对齐」)",
|
||
"where": "client/harmony/entry/src/main/ets/pages/MainPage.ets 的 PermissionTab(现在只读 /permission/pending)",
|
||
"kind": "scope",
|
||
"note": "**已修(2026-09-21)**。原文:鸿蒙只调 `/permission/pending`(SQL `WHERE pr.result IS NULL`)⇒ 已决策的历史完全看不到。\n\n修法与**为什么不照抄 WebUI 的 inbox 分组**:\n· WebUI 从 inbox 分组(`groupPermissions`),但它的 `PermissionRow` 只渲染\n `subject`/`created_at`/`permission_result`/`permission_expires_at`(逐字段 grep 过);\n· 而**待决**那一段我们要显示 `question`/`options`/`context`/`kind` —— 那四个字段在\n `permission_requests` **表**里,inbox 回包(`models.Mail`)**没有**(模型逐条核过)。\n⇒ 待决继续走专用端点(信息更全、能直接决策),**历史**走 inbox 补上。\n 代价:每账号多一次请求。这是有意的取舍。\n\n落地:`model/MailGrouping.ts` 加 `groupPermissions` + `PermissionGroup` + `isPendingPermission`;\n`PermissionTab.load` 取 inbox 里 `mail_type=permission_request && permission_result!=空` 的,\n分组后渲染「历史 n 条」。`cross-client-logic.test.mjs` 的 `gaps` 已按提示清空。"
|
||
},
|
||
{
|
||
"id": "harmony-morph-unverified-middleframes",
|
||
"count": 1,
|
||
"due": "有更快的取帧手段时(snapshot_display 往返 1.5-3s ⇒ 只能验 >=3s 的动画),或用户在真机上看过并反馈",
|
||
"where": "client/harmony/entry/src/main/ets/common/Motion.ets 的 Motion.morph;调用点 MailDetailPage / MainPage",
|
||
"kind": "env",
|
||
"note": "2026-09-21 加了两处**共享元素转场**(geometryTransition:回复球<->回复条、写信 FAB<->写信页),结构与接线都有判据钉住(animation-audit.test.mjs 6 条,三条变异逐个验过会红),但**动画本体在设备上没能看到**。\n\n原因:snapshot_display 一次往返 1.5-3s,uitest screenCap 约 3s,而这条动画 220ms ⇒ 探针比被测对象慢一个数量级。实测连拍 4 张,y=1600 的白区跨度全是 (30,1007)(每张都已是终态)。\n\n★ 这里的教训本仓已记过一次(「探针对被测变化不敏感时,量的是噪声」——那次我连测七八轮「没有中间帧」并编出三个错误理论,最后被位移探针推翻)。这次不再重复:**如实标未验**,不声称已看到。\n\n要验它需要:能按帧取图的手段(screenrecord 在本环境不可用),或真机上手看。"
|
||
},
|
||
{
|
||
"id": "harmony-account-errors-banner-unverified",
|
||
"count": 1,
|
||
"due": "有一个会取失败的账号可构造时(例如故意把某账号的 server 指向不可达地址)",
|
||
"where": "client/harmony/entry/src/main/ets/pages/MainPage.ets 的 accountErrors 横幅",
|
||
"kind": "env",
|
||
"note": "2026-09-21 接上了聚合失败横幅(WebUI MailList.tsx:85-96 的 account-errors)。修的是一处**安全网断线**:MailStore 一直在收集 snap.accountErrors(两处 load 都写),而界面从来没读过 ⇒ 某个账号拉不到邮件时列表**静默少一整份**,界面看起来完全正常,用户会得出错误结论(「没人给我发信」)。\n\n**只验了「不出现」那一半**:本机所有账号都取得到 ⇒ 横幅正确地不显示。「真的会显示成那样」没能实测 —— 需要一个取失败的账号,本机构造不出。\n\n为什么仍值得登记:这条横幅的**全部价值就在它出现的那一次**。「逻辑对齐 + 编译通过」与「真的渲染出来」是两件事,本仓为此反复吃过亏。"
|
||
},
|
||
{
|
||
"id": "harmony-permission-history-render-unverified",
|
||
"count": 1,
|
||
"due": "**有一个「未归档会话」里的已决策权限请求时**(光 `UPDATE mails SET permission_result` 不够 —— 见 note 里的实测)",
|
||
"where": "client/harmony/entry/src/main/ets/pages/MainPage.ets 的 permGroups 渲染段",
|
||
"kind": "env",
|
||
"note": "2026-09-21 闭合 harmony-permission-history 时新增的「历史 n 条」渲染。\n\n分组逻辑(`groupPermissions`)是纯函数、有跨端逐例判据;渲染那一层没验。\n\n★★ 2026-09-23 **实测订正复现路径**(原文写的是「`UPDATE mails SET permission_result` 之后即可复现」—— **那样做复现不出来**)。\n\n本机实测:库里**确实有 137 条已决策**的权限请求(`to_name=jianf` 107 条),但 `GET /me/mail/inbox?status=all` 返回的权限请求是 **0 条**。根因不在权限,在**会话归档**:\n · `repo.ListInboxScoped` 硬编码 `AND s.status <> 'archived'`(该过滤在整个 repo 出现 8 处,是「归档会话不进任何列表」的**全局约定**);\n · 而那 107 条所在会话**全部是 archived** ⇒ 被整条过滤掉;\n · 实测「未归档会话 + 已决策权限请求」的组合数 = **0**。\n\n ⇒ 鸿蒙的「历史 n 条」在**当前这台库上恒为空**,不是渲染坏了,是数据够不到。\n 两端一致(WebUI `PermissionList.tsx:66` 也是 `fetchInbox('all')`,同一端点同一过滤)⇒ 这不是鸿蒙的遗漏,**是两端共同的行为**,服务端的过滤是有意的。\n\n要真验,需构造:**未归档**会话 + 该会话里一条已决策的权限请求(例如 `UPDATE sessions SET status='active' WHERE session_id=<那条>`,或让 agent 在活跃会话里发一条再决策)。\n\n★ 另一处(同一次实测发现,已修):渲染注释曾承诺「会话别名 + **决策** + **时间**」,而代码只渲染「别名 + 历史 n 条」—— 属本仓反复在消的「声明比实现宽」。已把注释改成与 WebUI 折叠态一致的**真实形态**(WebUI 也是每组一行 `历史 {n}`,逐条的决策/时间要展开才看得到,而鸿蒙没有展开层)。"
|
||
},
|
||
{
|
||
"id": "permission-expires-at-unused",
|
||
"count": 1,
|
||
"due": "决定是否要把「可能已失效」做进**两端**(需要两端都补类型 + 渲染);或确认这个告警对产品不重要、把服务端那个字段也去掉",
|
||
"where": "服务端 `models.go:254`(`PermissionRequest.ExpiresAt`)/ `repo.go:1617`(`pr.ExpiresAt = models.PermissionDeadline(...)`);客户端类型两处都缺:`client/electron/src/types/index.ts` 的 `PermissionRequest`、`client/harmony/entry/src/main/ets/model/Models.ets` 的 `PermissionRequest`",
|
||
"kind": "scope",
|
||
"note": "★★ 2026-09-23 发现自己:**服务端返回一个两端客户端都不读的字段**。\n\n`GET /permission/pending` 的回包里 `expires_at` 一定存在(`ExpiresAt time.Time` 且**不带** `omitempty`),服务端在 `ListPendingPermissionsFor` 里用 `models.PermissionDeadline(pr.CreatedAt)` 算出它。而**两端的 `PermissionRequest` 类型都没有声明这个字段** ⇒ 反序列化静默丢掉。\n\n── 这次的教训与 `mail-list-attachment-count` 那次**方向相反、形状相同** ──\n那一次我把「`omitempty` 字段在一封没附件的邮件里缺失」当成了「服务端不返回这个字段」;这一次是**真的两端都没读**,而我一开始又差点写成「鸿蒙落后于 WebUI」——实际是**共同缺口**(WebUI 也没读)。两次都说明:**「两端不一致」与「两端都没做」必须先分清**,否则会去\"对齐\"一个根本不存在的东西。\n\n── 已经做掉的那一半 ──\n鸿蒙侧 2026-09-23 补上了 `expires_at` 并把它接进**待决卡片**的失效告警(`PermissionTab.ets` 的 `isStale`)。所以这条债现在**只剩 WebUI 那一半**:WebUI 的 `PermissionList.tsx:248` 读的是 `mail.permission_expires_at`(**别的字段**,那是 `Mail` 模型上由 `AttachPermissionDeadline` 算出来的,只在 inbox 路径上有),而它自己那份 `PermissionRequest` 同样没有 `expires_at`。\n\n★ 要闭合需先决定:**这个告警归哪条路径**?· 走 inbox(`permission_expires_at`)—— 但 inbox **只给未决策的**(`AttachPermissionDeadline` 的 return), 且 inbox 的 SQL **根本没选** `permission_expires_at`(实测 0 处), 它是在 repo 层算出来贴上去的 ⇒ 要确认它真的出现在回包里;\n· 走 `/permission/pending`(`expires_at`)—— 字段现成、语义清楚,但 WebUI 那边要改类型 + 读它。\n 我倾向后者(数据来源本来就对着\"待决\"这件事)。"
|
||
},
|
||
{
|
||
"id": "failure-suppression-must-not-merge-parallel",
|
||
"count": 1,
|
||
"due": "**失败报告抑制机制落地时**必须一并建(当前该机制**尚未实现** —— 服务端 grep `suppress|抑制` = 0,所以这条判据此刻无对象可测)。判据形状: 构造「同一父信下的两封并列失败报告」,断言 `relayed_mails` 里**两行都在**(而不是被 root-keying 压成一行)。",
|
||
"where": "待建。落点取决于抑制修法落在哪一层(`server/internal/repo/relayhops.go` 的 `CountTrailingRelayHops` 一族 / 发送侧桥);口径复算工具在 `deploy/recount-relay-counts.sh`",
|
||
"kind": "scope",
|
||
"note": "★★ 2026-09-25 pi 提出(`a948cdbb`)、我复核确认并登记。**这条钉的是机制,不是当下恰好成立。**\n\n## 当前为什么「看起来不需要它」\n```\nB 口径(agent, failure-父链根): 98 封 → 92 组、抑制 **6**\n 组内「两个成员共享同一父」的组数 = **0**(我逐组实测)\n ⇒ 5 个多成员组**全是真链式**(成员互为父子链),今天确实不会误合并\n```\n★ 但那是**当前数据的性质,不是设计的保证** —— 只要出现「同一封来信被两个不同 agent 各回一封失败报告」(或同一 agent 对同一封回两次),root-keying 就会把它们合并掉,而那些是**内容各异的并列失败**。\n\n## 这形状在本系统**真实发生过**(所以不是假想)\n```\nsession d042cc4c: 22 封失败报告、**22 个 parent 两两不同**\n (agent, 线程根) 口径 ⇒ 塌成 4 组(9/7/5/1)⇒ 一次**丢 18 封**\n 其中只有 1 个 parent 本身是失败报告 ⇒ **21 封是并列**\n```\n⇒ 「同级并列」不是边角情况。判据② 定稿即按此: **22 封并列失败、(线程根误用下)塌成 4 组、一次丢 18**。\n\n## 与已否掉的修法的关系(避免下一个人重走)\n```\n✗ 修法① 跳过 kind=summary —— 实测恰好豁免它要拦的那一类(该环 100% summary)\n✗ 修法②(仅根 root-keying) —— 就是本条要防的那个: 会吞并列\n△ relay_key 前缀判「是否失败报告」—— 诊断成立,但**服务端语义变更**,待人或宿主定\n```\n⇒ 本条不预设修法;它只要求: **无论选哪种,并列失败不得被合并**必须有判据。\n★ 这就是它该进登记而不是只留在邮件里的原因: 一个没有执行者的结论会一直「在讨论」,而登记 + 到期条件会让接手的人**必须**遇到它。\n\n## ★★ 2026-09-25 追加(我实测):本条的**前置**问题 —— \"是不是失败报告\"这个谓词**没有载体**\n\n在讨论\"用哪种 keying 抑制\"之前,先要回答\"**这封是不是失败报告**\"。我把它的**全部产生点**查了一遍 —— 它散在 **8 条语句 / 5 个文件 / 3 种语言**,且**写法互不相同**:\n```\nplugins/dsh-mail-bridge/src/index.ts:1240 model-failure:${data.mail_id}\nplugins/dsh-mail-bridge/src/index.ts:1749 **empty-reply**:${mailSessionID}\nplugins/pi-mail-bridge/src/worker.mjs:625 model-failure:${...}\nplugins/pi-mail-bridge/src/worker.mjs:715 model-failure:${...}\nplugins/zcode-mail-bridge/src/index.mjs:304 zcode-failure:${...}\nplugins/homeagent-mail-bridge/plugin.go:929 \"homeagent:failure:\"+replyTo ← Go\ndeploy/service-failure-notify.mjs:92/94 service-failure:${INVOCATION_ID|sha256} ← systemd 脚本\n```\n而**网关侧对它一无所知**: `grep -c` 这四家前缀 + empty-reply 于 `server/**/*.go` = **0**。\n\n★ 所以\"拿现成的 `RelayKeyForMail` 判一下就行\"只对一半 —— 它返回 `(relay_key, kind)`,而 **`kind` 区分不了失败**:\n```\nfailure 行的 kind 分布: kind=**summary** | 98 ← 全是 summary\nkind='summary' 内部: failure 98 / **非failure 321**\n⇒ 用 kind 判 ⇒ 会连 321 行普通搬运一起豁免 —— **正是已否掉的修法①**\n```\n\n★★ **活的反例**(这条最关键): `empty-reply:` 是失败类但**不含 `failure` 字样** ⇒ `LIKE '%failure%'` **永远抓不到它**(该路径代码 1 处、当前 **0 行** ⇒ **将来第一次触发就是静默漏判**)。\n\n⇒ 推论: 若在网关里硬编码这四家前缀,第 6 家桥出现时**静默漏判**,后果是\"报告不再被抑制\" ⇒ 环回来(**假绿**)。\n⇒ 所以修法应先把**分类变成网关拥有的东西**(relay 枚举加第三类 / 或 relayed_mails 加一列),再由各桥**声明**而非拼串。\n\n⚠ 加枚举会撞上一条**故意**的锁: `TestRelayKindsIsExactlyTwo`(`relay_test.go:60-64`)\"免配额类型是白名单…新增前请确认它确实是 harness 代劳\" ⇒ 必须同步改它,而那正是它**本来就该问**的问题(failure 确实是 harness 代劳)。\n\n## ★★ 2026-09-25 追加(判据清单落定):⑥ permission 分叉点,**必须**钉住\n\npi `8f6f6e6e` 提出、我 `476d22ad` 复核后定为判据清单的第六条。\n\n### 分叉点是什么\n```\npi 的判据: 「本封来信 id ∈ relayed_mails」 ⇒ 抑制\n我的判据: 「parent 本身是 failure-relay」 ⇒ 抑制\n构造: 一封 permission 询问(kind=permission,也是 relay)发出\n → 对方处理它时失败 → 发失败报告\n pi 的判据: 来信(permission) ∈ relayed_mails 为真 ⇒ **抑制**(错: 首报该发出去)\n 我的判据: parent 不是 failure 类 ⇒ **放行** ✓\n```\n### 可达性我验了(这是它必须钉住的原因)\n```\npermission-relay 的子邮件 = **137**(mail_type: normal 58 / permission_decision 79)\n ⇒ 子邮件**确实会被产生** ⇒ 只要那一侧处理失败,就会产出失败报告 ⇒ 分叉点可达\n当前: failure 报告的 parent 是 permission-relay 的 = **0**\n ⇒ 今天**还没分叉** —— 而这正是\"两条判据现在等价(都 10 / 差集 0)\"的来源\n```\n★ 所以\"两条判据等价\"是**当前数据的性质,不是机制的保证** —— 与 pi 上封(B 口径今天干净、A 口径的 9 封并列已证明同级并列真实存在)**同一句**。\n⇒ 选判据的理由**不能是\"它们现在等价\"**,而是: **我的判据把\"是不是失败类\"这件事读了出来,pi 的没有** —— 信息量更大的一点更耐久。\n\n### 判据清单(落定版)\n```\n① 环: hop2 起被抑制 ⇒ 环里只剩 1 封\n② 反例: d042cc4c 的 22 封并列失败**一封都不能被抑制**\n③ homeagent 风格 14 封(标题正常但是失败报告)必须被认出 —— 只有元数据能过\n④ 首报必须送得出去(防全抑制)\n⑤ 同一父信下的两封并列失败报告不得被合并(本条的主体)\n⑥ **permission_request 首次失败时,其失败报告必须发出**\n (parent 是 permission-relay,不是 failure-relay)—— 需要失败(构造)\n```\n### ⚠ 一条被否掉的落点细节(免得下一个人重走)\n```\npi 建议: \"插在 :384(CountTrailingRelayHops)之前 ⇒ 被抑制者不消耗 hop,这个口径要写进判据\"\n⇒ 我读实现后判它为**假问题**: `CountTrailingRelayHops` 是**重算**(relayhops.go:54-80\n 每次按 mails LEFT JOIN relayed_mails **现扫**),不是自增计数器\n ⇒ \"被抑制者是否计入 hop\" 不是口径选项,而由\"**有没有落库**\"唯一决定: 没落库 = 不计\n ⇒ ~~**不该写进判据** —— 写进去会误导后来的人以为这是个可配的选择~~\n ⇒ ⚠️ **这一条我判错了 —— 它已被本节末的\"三个带\"取代(请读下去,别停在这里)**:\n 实测抑制点有 ③ 个带,最坏那个会**清零 hop 守卫** ⇒ 不但要写进判据,\n 还要写成**顺序约束**(\"必须在 CountTrailingRelayHops 之前 return\")。\n### ★★ 但\"插在 :384 之前\"这个落点本身**不是无关紧要的** —— 我上一条的\"假问题\"判过头了\n\n我原先写\"计不计 hop 由有没有落库唯一决定 ⇒ 不该写进判据\"。**那半对,但漏了两个形** ——实测(`/tmp/hop.db` 合成库,mails LEFT JOIN relayed_mails 现扫):\n```\n基线 3 封全绑: 序列 1 1 1 ⇒ hops = 3\n加 1 条**占位行**(mail_id NULL,无 mail 行): 序列 1 1 1 ⇒ hops = 3 **不受影响**\n加 1 封**有 mail 行但未绑 relay**: 序列 **0** 1 1 1 ⇒ break ⇒ **hops = 0**\n```\n⇒ 所以抑制点的位置有**三个带**,后果完全不同:\n```\n① 在 :384(CountTrailingRelayHops)之前 return ⇒ 该封既不落 mail 也不落 relay ⇒ 真正\"不计\" ✓\n② 在 :396(ClaimRelay)之后、:474(CreateMail)之前 ⇒ 落**占位行** ⇒ hop 不变,但**白占幂等键** ⇒ 后续重试被挡\n③ 在 :474(CreateMail)之后 ⇒ 落**有 mail 行、无 relay 绑定** ⇒ is_relay=0 ⇒\n `CountTrailingRelayHops` **break ⇒ hops 归零** ⇒ **hop 上限对该会话失效**(那封无绑定邮件停在最新位)\n```\n★ ③ 是最坏的: 它不是\"多算或少算 1\",而是**主动清零守卫** —— 环因此可以**绕过 5 跳上限**。\n而 ③ 正是\"抑制逻辑写在 CreateMail 之后\"这种最自然的写法会落进去的带(因为那里才拿到 mailID)。\n\n⇒ 结论(取代我上一条): **这个口径必须写进判据,但写法不是\"计不计\"** —— 而是\n```\n「抑制必须在 CountTrailingRelayHops 之前 return」\n 理由: 落在其后会造出\"有 mail 行无 relay 绑定\"的邮件 ⇒ 把 hop 守卫**清零**\n 判据形状: 构造一封因抑制而不发的报告,断言同会话 CountTrailingRelayHops 不降为 0\n (这条**能失败** —— 把抑制挪到 CreateMail 之后就红)\n```\n★ pi 的直觉(\"位置决定 hop 读数,所以要写进判据\")**是对的**,我错在把它当成\"可配的口径\"而整体否掉。正确的是: 它不是口径,是**必须满足的顺序约束**,而顺序约束**更要**进判据。\n```"
|
||
},
|
||
{
|
||
"id": "platform-mirror-replace-domain-too-wide",
|
||
"count": 1,
|
||
"where": "`server/internal/repo/platform_sessions.go`:`ReplacePlatformSessions` 的 `DELETE ... WHERE agent_name = $1`(:63)与 `INSERT`(:79)—— 替换域=**agent**,而每个上报者只知道**一个 directory**(`plugins/opencode-mail-bridge/index.js:1147` `client.session.list({ query: directory ? {directory} : undefined })`)。复现读数:`sqlite3 --readonly /opt/agentmail/data/agentmail.db \"select workspace,count(*) from agent_platform_sessions where agent_name='opencode' group by workspace;\"` —— 连续采样会看到它按 project 轮换(实测 10 个状态)。同族另一处:`server/internal/handler/agents.go:185` 的 `req.PlatformSessions != nil` 对 `[]` 为真 ⇒ 空清单也走整表替换。",
|
||
"due": "**决定「一个 agent 一个镜像桶」还是「一个 (agent, workspace) 一个桶」之时**(即修这个缺陷的那一次)。★ 前置条件(实测而非推断):**必须先动主键** —— `PRIMARY KEY (agent_name, platform_id)`(`server/internal/db/migrations/init_sqlite.sql:421`)若不加 workspace,则「同一 platform_id 出现在两个 workspace」会 `UNIQUE constraint failed (1555)`,而 INSERT 无 `ON CONFLICT`(grep=0)+ `defer tx.Rollback()` ⇒ **整个 DELETE 回滚**、`agents.go:186` 降级为 -1、桥侧不读该字段(grep=0)⇒ 三重静默,表现为「心跳一直成功而镜像永久停滞」(比现在的间歇擦除**更难查**)。顺序:先 PK 加 workspace,再 DELETE 加 workspace。",
|
||
"kind": "scope",
|
||
"note": "2026-09-26 我(dsh)与 pi 在 `21c398ee` 会话上共同定位;**尚未实现修法**,故记欠账。\n\n## 现象(可复现)\n```\n同一文件/同一 inode 的 agent_platform_sessions 在若干状态间轮换,差的恒为一个 project 的会话数:\n 实测 10 个状态: /tmp=49 /root=38 am-mcp-probe=23 agentmail=37 TrueAgent=100\n llmsproxy=18 facemodule=7 LiquidUnifiedDebugEngine=7 NextAgent=2 (空)\n每次替换是**整表**(同状态内 37 行的 reported_at span = 0.0ms)\n```\n\n## 危害(口径已修正)\n```\n候选列表有两个来源: 来源1 = 本侧 sessions(实测 6 条)、来源2 = 该镜像表(实测 37 条)\n⇒ 镜像被别人擦掉时,该 workspace 的候选从 ~43 掉到 ~6(掉 37 条)—— **不是归零**\n (我先前写成 37→0,是漏了来源1;数字口径缺口径,已更正)\n```\n\n## 三个曾被提出、但已被推翻的根因(留作反面材料)\n```\n① 「读域 vs 擦除域不等,且 [] 是 truthy」——只解释 5 个 0 会话 project(实测 45 次里 9 次空),\n 非主因(非空替换占 36/45 = 80%)\n② 「PK 允许并集 ⇒ 擦除在语义上不必要」——**错**。整表替换是**有意设计**且有测试钉着:\n `server/internal/repo/platform_sessions_test.go:219` `TestReplacePlatformSessionsIsFullReplace`\n + 函数文档 :44-46 明写要防「平台删了会话却留在镜像 ⇒ 选了 404」\n ⇒ 擦除**必要**,错的是**范围**(应 per-source replace,即「擦的域 == 读的域」)\n③ 「今天不撞 PK 是因为 session.id 全局唯一」——**因给错了**。实测: 同一 platform_id 换 workspace\n **也不撞**(err=nil)⇒ 真正的原因是两处**代码结构**:\n · 跨调用: `DELETE WHERE agent_name=$1` 先清该 agent 全部行(:63)\n · 同调用内: `seen` map 去重(:70)\n 与 id 是否唯一无关 ⇒ 「数据性质 vs 约束」那个框架本身对,载体是这两处代码。\n```\n\n## 为什么这条值得进登记(而不是只留在信里)\n```\n它是一个**已定性的、带顺序约束的**修法前提: 不是「顺手改一下」,而是\n「先改 PK、再改 DELETE,否则新的写法会从间歇擦除变成**永不自愈的静默停滞**」——\n而后者没有报错、没有日志、桥也不读响应(三重静默),下一个人只会看到「补全里少了会话」。\n★ 该断言的形状(而非点名三笔)已在 debt_registry_test.go 里确立,故新增条目不触碰既有断言。\n```\n"
|
||
}
|
||
]
|
||
}
|