Commit Graph

3 Commits

Author SHA1 Message Date
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
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
f9d757b5e5 chore: directory migration - gateway→server, web→client/electron 2026-09-08 19:16:35 +08:00