diff --git a/docs/DSH-0.1.5-MAIL-CHANNEL-ROOTCAUSE.md b/docs/DSH-0.1.5-MAIL-CHANNEL-ROOTCAUSE.md index a958f73..da0d007 100644 --- a/docs/DSH-0.1.5-MAIL-CHANNEL-ROOTCAUSE.md +++ b/docs/DSH-0.1.5-MAIL-CHANNEL-ROOTCAUSE.md @@ -83,7 +83,7 @@ const event = this.session.append("agent/inbox/spliced", splice); | 验证层 | 方法 | 结果 | |---|---|---| | 迁移链 | 官方 `sessionFormatCatalog.createRestore` 跑真实 v0 字节 | 修前 FAIL → 修后 OK | -| 严格档 | 同链 `validation:"current"` | 修后仍 `seq gap` ⇒ 见 §5 | +| 严格档 | 同链 `validation:"current"`(**深拷贝**) | 修后 **OK(40/40)** —— 见 §17 的更正 | | 生产档 | `recovery:"recoverable", validation:"transformed"` | 修后 OK | | **端到端** | **`JsonlSessionPersistence.open(id,"read")` 真读盘** | **修前 FAIL → 修后 READ OK** | | 批量 | 对全部 `mail-*` v0 会话跑真读路径 | **before 0 / after 33 可读**(agentmail 等 12 个 store) | @@ -114,20 +114,30 @@ pi 的「其它 34/28」也复现了,且能解释:28 个读不了的里 - 给 `inserted[]` 中缺 id 的消息补 `id` + `role: "user"`; - **不要**同时补 `user/message`(会引入 `seq gap`)。 + → ⚠️ **后半句已作废**:见 §17。「补 user/message 会引入 seq gap」同样是 + **同一批对象被验证器污染**造成的假象;深拷贝重测后,补与不补都 strict-ok。 32 个 `subagent/descriptor version 2` 是**另一个**问题,别混进同一个 repair。 -## 5. 必须记一笔:`id` 是伪造的,会污染未来的严格校验 +## 5. ~~必须记一笔:`id` 是伪造的,会污染未来的严格校验~~(**已作废,见 §17**) -补进去的 `id` 是**新造**的(`recovered-splice--`),原数据里没有。 -证据:修后在**同样字节**上只换校验档,结果不同—— +> ⚠️ **本节结论是错的,2026-09-19 下午用「深拷贝」重测后推翻。** +> 保留原文只为留痕:它正是 §10/§17 那个「校验会原地改写入参」陷阱的第一个受害者。 +> 正确结论:伪造的 `id` **不会**污染严格校验,修后会话 **strict 也读得出来**。 + +~~补进去的 `id` 是**新造**的(`recovered-splice--`),原数据里没有。~~ +~~证据:修后在**同样字节**上只换校验档,结果不同——~~ ``` [recovered-splice / transformed] OK [recovered-splice / current] FAILED: released Session row 35 has seq gap (expected 99, got 72) ``` -⇒ 修出来的会话是**生产档可读、严格档不可读**。 +⇒ ~~修出来的会话是**生产档可读、严格档不可读**。~~ + +**这段"证据"的真实成因**:先跑了一次 `transformed`(在**原对象**上), +验证器把补全的 `dt` 写回了那批对象;随后拿**同一批已被污染的对象**跑 `current`, +于是失败。换一批干净对象,两个档位都 OK。**是测量方法错,不是数据错。** **这意味着:read-only 的修复(能读老线索)是稳的;但若要把它作为"可继续写入的活会话", 在 v3 严格校验下仍有风险。** 建议: @@ -157,9 +167,11 @@ pi 的「其它 34/28」也复现了,且能解释:28 个读不了的里 1. **不是"旧数据不兼容"**——是 `agent/inbox/spliced` 的 `inserted[]` 缺 `id`/`role`, 且**当前版本仍在写**这个形状。 -2. **只补 spliced,别碰 `user/message`**——补后者会把可读会话变成 `seq gap`。 -3. **修出来的会话生产档可读、严格档不可读**——适合捞回老线索, - 不适合当长期可写会话;写入侧规范化才是治本。 +2. ~~**只补 spliced,别碰 `user/message`**——补后者会把可读会话变成 `seq gap`。~~ + → **已作废(§17)**:深拷贝重测后补 `user/message` **无害**(40/40 strict-ok); + 当时看到 `seq gap` 是「校验污染了入参」的假象。 +3. ~~**修出来的会话生产档可读、严格档不可读**~~ → **已作废(§17)**: + 修后会话**两档都可读**(40/40)。 --- @@ -282,3 +294,65 @@ dsh 观察到 `readSession()` 成功、`JsonlSessionPersistence.open()` 失败 ⇒ **「真 mail-* 会话 43/43 可读,0 不可读」**。 度量口径本身也会制造假结论,所以脚本现在两个开关都提供: `--only`(子串,跑路径片段)与 `--prefix`(目录名严格前缀,点名某类会话时用后者)。 + +## 17. 更正 §4 与 §5:那两条"风险"是**测量方法**造成的,不是数据性质 + +**触发**:pi 在回信里指出 §10 那个坑还有下半段 —— +「同一个对象既校验又落盘是个静默陷阱」。我据此把**自己的**两条结论重测了一遍, +**两条都推翻**。留档如下,因为它们恰好演示了同一个陷阱的第二次咬人。 + +### 17.1 更正一:伪造的 `id` 不会污染严格校验(§5 作废) + +§5 的证据是「同样字节只换校验档,结果不同」。真实成因: + +| 步骤 | 做法 | 结果 | +|---|---|---| +| ① | `transformed` 跑在**原对象**上 | OK(但验证器**已把 `dt` 写回原对象**) | +| ② | 拿**同一批被污染的对象**跑 `current` | FAIL: `seq gap` ← §5 引用的就是这条 | + +正确测法(每次都给 `structuredClone`): + +``` +A) strict-first, fresh clone : OK +B) transformed on originals, then strict + on SAME objects : FAIL: row 35 seq gap ← 复现出那个假象 +C) transformed(clone) then strict(clone) : transformed=OK strict=OK +``` + +并且**不换档位、只重跑**也是稳定的(同进程 3 次 × 3 个 OS 进程,strict 全 OK): +⇒ 这不是"不确定性",就是**入参被上一次校验改过**。 + +批量重测(深拷贝)**全部邮件会话**: + +``` +mailInjected strict-ok 40 strict-FAIL 0 (n=40) <- 修过的 +otherUntouched strict-ok 37 strict-FAIL 0 (n=37) <- 从没修过的对照组 +``` + +**从没修过的会话 strict 也全 OK** ⇒ 「strict 失败」与"伪造 id"无关。 + +### 17.2 更正二:补 `user/message` **无害**(§4/§8 那条作废) + +同样用深拷贝重测 §4 那句「补 `user/message` 会把可读会话变成 `seq gap`」: + +``` +mail-* v0 需修复的会话: 40 + spliced-only -> strict-ok: 40/40 + spliced+both -> strict-ok: 40/40 <- 补 user/message 一样通过 +``` + +更关键的是**迁移器本来就会给 `user/message` 合成 id**: +对**修复前的原始 v0**(25MB 那份)跑迁移,出来的 `user/message` 与 `inserted` +**带 id 386 条 / 缺 id 0 条**。 +⇒ 「严格读路径要 id」与「别补 spliced」**并不冲突** —— id 是迁移器给的, +需要我们补的只有 `inserted[]`。pi 上封担心的"两个口径各要一份不同东西"**并不成立**。 + +### 17.3 结论(覆盖 §4/§5/§8) + +1. 修复效果比原先说的**更好**:修后会话 **`transformed` 与 `current` 两档都可读**, + 不只是"生产档可读"; +2. 伪造 `id`(`recovered-splice-*`)**没有**留下严格校验隐患; + 实测换成真 `randomUUID()` 或官方风格 id,两档同样 OK —— **id 的取值形式不影响结论**; +3. 真正该记的教训不是"数据有隐患",而是**"校验会改写入参,所以校验与落盘必须分对象"**—— + 它已经分别制造过 §10(dry-run 40 vs apply 3)和本节(假 `seq gap`)两次假结论。 +4. 仍然成立的独立缺陷:`subagent/descriptor ... version 2`(29 个非邮件会话),与本问题无关。