更正: 我自己的两条风险是**测量方法**造成的假象 —— 校验会原地改写入参,换个对象就消失(§17)

pi 指出 §10 那个坑还有下半段。我据此重测了自己的两条结论,**两条都推翻**:

1. §5「伪造 id 会污染严格校验」—— 假象。§5 引用的 seq gap 来自
   「先 transformed(在**原对象**上,验证器已把 dt 写回)→ 再拿同一批对象跑 current」。
   深拷贝重测:strict-first OK、transformed(clone)+strict(clone) OK;
   同进程 3 次 × 3 个 OS 进程全 OK ⇒ 不是不确定性,是入参被上一次校验改了。
   **从没修过的 37 个对照组会话 strict 也全 OK** ⇒ 与伪造 id 无关。

2. §4/§8「补 user/message 会引入 seq gap」—— 同样假象。深拷贝重测:40/40
   补与不补都 strict-ok。且迁移器**本来就会**给 user/message 合成 id
   (原始 v0 迁出 386 条带 id / 0 条缺 id)⇒ pi 上封担心的
   两个口径各要一份不同东西并不成立。

结论比原先更好:修后会话 **transformed 与 current 两档都可读**,
伪造 id 换成真 randomUUID() 结论不变(id 取值形式不影响)。

真正该记的是:**校验会改写入参 ⇒ 校验与落盘必须分对象**。它已制造
§10(dry-run 40 vs apply 3)与本节(假 seq gap)两次假结论。

另独立复核了数据完整性(40/40):.bak 齐全、事件数一致(80001=80001)、
剥掉注入的 id/role 后逐行语义差 0、87 个注入 id 全唯一。
This commit is contained in:
2026-09-19 12:15:45 +08:00
parent fe0cfe626c
commit f236ef44ca

View File

@ -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-<seq>-<n>`),原数据里没有。
证据:修后在**同样字节**上只换校验档,结果不同——
> ⚠️ **本节结论是错的,2026-09-19 下午用「深拷贝」重测后推翻。**
> 保留原文只为留痕:它正是 §10/§17 那个「校验会原地改写入参」陷阱的第一个受害者。
> 正确结论:伪造的 `id` **不会**污染严格校验,修后会话 **strict 也读得出来**。
~~补进去的 `id` 是**新造**的(`recovered-splice-<seq>-<n>`),原数据里没有。~~
~~证据:修后在**同样字节**上只换校验档,结果不同——~~
```
[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 个非邮件会话),与本问题无关。