dsh-engram 0.8.7 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -264,7 +264,7 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
264
264
  <!-- dsh-market compatibility block — keep labels bilingual and in sync with
265
265
  docs/RELEASE-NOTES.TEMPLATE.md (market README convention) -->
266
266
 
267
- - **DSH 版本 / DSH versions**: `>=0.1.2-alpha.2 <0.3.0-0` (本版 `0.8.7`; 已发布的 `0.3.7`、`0.4.0` 仍声明旧的 `0.1.x` 范围 `>=0.1.2-alpha.2 <0.2.0-0` — 见下方矩阵)
267
+ - **DSH 版本 / DSH versions**: `>=0.1.2-alpha.2 <0.3.0-0` (本版 `0.9.0`; 已发布的 `0.3.7`、`0.4.0` 仍声明旧的 `0.1.x` 范围 `>=0.1.2-alpha.2 <0.2.0-0` — 见下方矩阵)
268
268
  - **最后验证日期 / Last verified**: 2026-10-27 — DSH `0.2.1-rc.2` (`dsh --version`), web profile live composition; npm test 483/483
269
269
  - **安装方式已验证 / Verified install**: `dsh plugin --profile web add dsh-engram`
270
270
 
@@ -294,7 +294,9 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
294
294
  | `0.8.3`–`0.8.4` | 本地 | 同 `0.5.0` — 审批门(v0.8-D)+ 其 GUI;G6 门槛改单边(Δ ≥ −2pp)+ 隐藏规格语料 | 同 `0.5.0` |
295
295
  | `0.8.5` | 本地 | 同 `0.5.0` — 记忆行文件信号(`⚠file-changed` / `⚠file-missing`),G6 第三/四轮(撤回 +30pp,Δ=+58.3pp) | 同 `0.5.0` |
296
296
  | `0.8.6` (npm) | published | 同 `0.5.0` — 修 issue #9:宿主没有 `sessionQuery`/`snapshotEvents` 时退回 `seq`+`eventAt` 逐条读,四项注入特性不再静默休眠;warn 报出宿主 surface;G6 第五轮 Δ=+90.0pp | 同 `0.5.0` |
297
- | `0.8.7` (本期 / this release) | 本地 | 同 `0.5.0` — 推送腿落盘(`$DSH_HOME/engram/push-legs.jsonl`)+ `status` 打印事件日志读法;`minimal` 预设不再承诺永远不会发生的自动接管 | 同 `0.5.0` |
297
+ | `0.8.7` (npm) | published | 同 `0.5.0` — 推送腿落盘(`$DSH_HOME/engram/push-legs.jsonl`)+ `status` 打印事件日志读法;`minimal` 预设不再承诺永远不会发生的自动接管 | 同 `0.5.0` |
298
+ | `0.8.8` | 本地 | 同 `0.5.0` — 修推送腿遥测写错目录(`engram/engram/`)导致状态显示「还没记录」;`pushLegsPath` 契约写死在一处 | 同 `0.5.0` |
299
+ | `0.9.0` (本期 / this release) | 本地(tag,未发 npm) | 同 `0.5.0` — 产品代码仅一条 CLI 修复(`--presets-dir` 优先);主体是评测:G6 网络围栏(per-uid 出网白名单 + 两条前置探针)、密钥语料第 4 条自证、污染三分类与重算、G7 实测 0.2956%。**不是 1.0**:宿主仍 rc、G1 未测、减法债未还 | 同 `0.5.0` |
298
300
 
299
301
  ### Where the API family broke — `0.1.2-alpha.2`
300
302
 
package/README.zh.md CHANGED
@@ -189,7 +189,9 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
189
189
  | `0.7.5`–`0.7.13` | 本地 | 同 `0.5.0` —— 账本按会话落行 + `gate-audit`;L1 从「自己剪」转成「记账 + 归因」;压缩入账读真实事件形状;会话计数重启不清零;CLI 尊重 `DSH_HOME` | 同 `0.5.0` |
190
190
  | `0.8.0`–`0.8.5` | 本地 | 同 `0.5.0` —— 证据化 procedure、导出 DSH Skill、subagent 交接胶囊、审批门 + GUI、记忆行文件信号(`⚠file-changed`/`⚠file-missing`)、G6 门改单边 + 隐藏规格语料(Δ=+58.3pp) | 同 `0.5.0` |
191
191
  | `0.8.6`(npm) | 已发布 | 同 `0.5.0` —— 修 issue #9:宿主不暴露 `sessionQuery`/`snapshotEvents` 时退回 `seq`+`eventAt` 逐条读,四项注入特性不再静默休眠;warn 报出宿主 surface;G6 第五轮 Δ=+90.0pp | 同 `0.5.0` |
192
- | `0.8.7`(本期 / this release) | 本地 | 同 `0.5.0` —— 推送腿落盘(`$DSH_HOME/engram/push-legs.jsonl`)+ `status` 打印事件日志读法;`minimal` 预设不再承诺永远不会发生的自动接管 | 同 `0.5.0` |
192
+ | `0.8.7`(npm) | 已发布 | 同 `0.5.0` —— 推送腿落盘(`$DSH_HOME/engram/push-legs.jsonl`)+ `status` 打印事件日志读法;`minimal` 预设不再承诺永远不会发生的自动接管 | 同 `0.5.0` |
193
+ | `0.8.8` | 本地 | 同 `0.5.0` —— 修推送腿遥测写错目录(`engram/engram/`)导致状态显示「还没记录」;`pushLegsPath` 契约写死在一处 | 同 `0.5.0` |
194
+ | `0.9.0`(本期 / this release) | 本地(tag,未发 npm) | 同 `0.5.0` —— 产品代码只有一条 CLI 修复(`--presets-dir` 优先);主体是评测:G6 网络围栏(per-uid 出网白名单 + 两条前置探针)、密钥语料第 4 条自证、污染三分类与重算、G7 实测 0.2956%。**不是 1.0**:宿主仍 rc、G1 未测、减法债未还 | 同 `0.5.0` |
193
195
 
194
196
  ### API 族断裂点:`0.1.2-alpha.2`
195
197
 
@@ -538,3 +538,509 @@ artifact:`eval/g6/runs/r5-confined/runs.json`(40 行,含每行 `pass/exit/
538
538
  - 不能说:通用编程能力提升 90pp。测的仍是一条**信息通道**。
539
539
  - 边界:单模型(本地 `deepseek-v4-flash`)、4 任务、5 种子、一臂一组种子记忆。
540
540
  Δ 的量级受任务选择影响极大,方向可靠,幅度别外推。
541
+
542
+ ## 13. 第六轮:把臂名从臂能看到的一切地方抹掉(2026-10-28)
543
+
544
+ §12 留下的那句话是这一轮的起点:Δ=+90pp 测出来了,但**臂名写在 cwd 里**,8/20 的控制
545
+ 会话据此推理过。本轮的目标只有一个——**让 Δ 在一个控制臂不可能知道自己属于哪一臂的
546
+ 环境里成立**。
547
+
548
+ 这一轮跑了三次才成,而且三次的失败都不是模型的问题,是**评测工具自己的窟窿**。按顺序
549
+ 摊开,因为它们比最后的数字更值得记住。
550
+
551
+ ### 13.1 设计(与 §12 同构,只换名与围栏)
552
+
553
+ | 项 | 值 |
554
+ |---|---|
555
+ | 语料 | `eval/fixtures/ledger-kata-blind`(4 任务:`codec` `errcode` `money` `ranking`) |
556
+ | 种子记忆 | `eval/g6/seed-memory-blind.json`(含一条**过期**的 `[stale:ranking]`,门第二句要它) |
557
+ | 配对 | 4 任务 × 5 种子 = **20 对**,`--control-first` |
558
+ | 两臂 | `p-a`(带记忆)/ `p-b`(裸)。目录名 `arm-a`/`arm-b`,映射只写在 artifact 的 `armLabels` |
559
+ | 隐藏 | `--hide-store-for p-b`:控制臂运行时 store 改名后**由围栏把 `storages/` 整个挂空**(同目录改名不是隐藏;移到 `$TMPDIR` 在 Linux 上跨设备 `EXDEV`) |
560
+ | 围栏 | `--sandbox`:按平台选机制——macOS seatbelt(默认收禁 + 放行白名单)、Linux bubblewrap(`--ro-bind / /` + 挂空 + 放行白名单)。两侧都禁 `/tmp`、`$HOME`、本仓库,只放行本臂 ws / 本臂 profile / 启动器 / 凭据;另一臂 profile 再禁一次。见 §13.6 |
561
+ | 环境 | Linux Ubuntu(内核 6.8)、node v22.22.0、`dsh` 0.2.1-rc.2、`dsh-engram` **0.8.8**(与第五轮的 0.8.2 不同代,见 §13.4 的"不能说") |
562
+ | 日志 | `--log-dir`:run.log 与 check 输出一起移出 run 树,且整个目录对臂不可读 |
563
+ | 判据 | 预注册留一法 `Δ ≥ −2pp`(工具内置),配对下限 10 |
564
+
565
+ ### 13.2 为了让"无泄漏"成立,修掉的十三道缝
566
+
567
+ 每道都有证据,都不是"顺手优化"。
568
+
569
+ | # | 缝 | 证据(谁发现的) | 修复 |
570
+ |---|---|---|---|
571
+ | 1 | cwd 里就是臂名 | §12 的 8/20(第五轮已知) | 目录 `arm-a`/`arm-b`;本轮再把 profile 名换成 `p-a`/`p-b` |
572
+ | 2 | **种子键没解析**:`--out` 写 `/tmp/…`,插件按 `getcwd()` 记 `/private/tmp/…` | 冒烟:治疗臂引不出任何 `[seed-*]`,跟控制臂一个水平 | `seedStore` 用 realpath 后的 key;测试钉住"种子行必须带臂自己会算出的写法" |
573
+ | 3 | `--resume` 名不副实(TDZ 异常被吞,打印成"没有可续的行") | 第四轮 ENOSPC 后是靠人手重跑的 | 挪到 plan 之后;日志打印复用了多少行 |
574
+ | 4 | 产物只在跑完后写一次 | 第六轮第一次:16/40 行时 ENOSPC,1.7 小时蒸发 | 每行原子落盘(`partial: true`)+ 开始前空盘预检(< 512MB 直接拒绝) |
575
+ | 5 | **臂能 `ls` 实验根**:祖先目录给的是 `file-read*`,在目录上就是"可枚举" | 第二次的控制日志:`ls /private/tmp/g6r14` 看到 `runs.json`/`arm-a`/`arm-b`,10/20 开始推理"这是评测矩阵" | 祖先只给 `file-read-metadata`(getcwd/realpath 仍活);产物搬到 `--log-dir` |
576
+ | 6 | **共享 home 泄漏**:两臂共用 `DSH_HOME`,插件自己的记账文件写着另一臂 | 第三次:`recall-injections.json` 里 `lastWorkspace: /private/tmp/g6-smoke3/arm-a/…`;`ledger.json` 按 workspace 路径记 `injectedChars`;7/7 控制日志引用了 `profiles/ab-engram` | `scrubSharedHome()`:矩阵开始前清 sessions / injections / context-gc / ledger;profile 改中性名 |
577
+
578
+ | 7 | **插件包自带 `docs/`**:`dsh-engram` 的 tarball 把这一轮的评测文档一起装进治疗臂自己的 `node_modules`,而 `docs/G6-quality-ab.zh.md` 第 403 行**原文写着 codec 的公式** | 移植时读安装树:`profiles/p-a/node_modules/dsh-engram/docs/G6-quality-ab.zh.md:403` | 装完即删;并把两侧围栏都加一条"本臂包内 `docs/` 挂空"(重装也不会悄悄开口)。测试钉住"自己包里的语料文档读不到" |
579
+ | 8 | **语料写死上一台机器的绝对路径**:4 条 `check` 全是 `node /Users/sunchuxiong/dsh-loom/eval/g6/check.mjs …` | 移植后第一次冒烟:两臂 `fail`,`MODULE_NOT_FOUND`;换成无围栏跑也会这样,因为问题在判分器不在臂 | 语料改用 `${REPO}` 占位符,工具展开成当前 checkout;并加一条**拒绝**规则:`check` 里出现 oracle 文件却不带占位符就拒绝开跑(否则两臂全 0 → 闸门判"两臂持平 PASS",是静默假结论,不是报错) |
580
+ | 9 | **臂继承了调用者的身份**:矩阵本身由一个 dsh 会话启动,`process.env` 里带着 `DSH_PROFILE_DIR`/`DSH_SESSION_ID`/`DSH_PROFILE=web`/`DSH_SHELL=1` | 起跑前 `env \| grep DSH_`:臂 `env` 一敲就知道自己在别人 `web` profile 的会话里,且 `DSH_SHELL`/`DSH_PROFILE_DIR` 会影响 dsh 怎么解析自己的 profile 与策略 | `armEnv()`:两臂只拿到 `DSH_HOME`,其余 `DSH_*` 全部剔除。测试断言臂看到的 `profile=/session=/shell=` 都是空 |
581
+ | 10 | **`--control-first` 的比较器永不返回 `-1`**:`sort((a,b) => (a是控制?0:1) - (b是控制?0:1))` | 读代码:这个比较器只说"不小于",排序结果取决于引擎实现;而顺序**就是**隔离本身(控制跑在治疗之后就能读到治疗的 session) | 改成显式分区(`filter` 两遍拼起来)。测试断言 20 行里控制臂全部排在前 |
582
+ | 11 | **仓库"恰好"被藏住**:产物目录在 `$HOME` 下,于是被 home 那条规则连带挂掉——换个 checkout 位置就漏 | 移植时把 checkout 换到 `$HOME` 之外,读 `buildBwrapArgs` 发现没有针对 repo 的规则 | `repo` 与会话目录一样显式挂空(在 `$HOME` 内则不重复挂)。测试钉住"外层挂空里必须有 checkout" |
583
+ | 12 | **两臂可能指向不同的模型端点**:官方端点跑到一半没钱,最自然的"补救"是把剩下 7 行换到本地 vLLM 补齐——那样 40 行产物里的 Δ 一半是插件、一半是服务栈,而产物、日志、报表里**看不出来** | 自己想补跑时发现的:配对比较只在端点内部成立 | `assertSameTarget()`:`run` 开头对两臂各跑一次 `--dump-config`,把 `agent-default-model` 的 `provider`/`model` 比出来,不同就拒绝开跑;产物里记 `target`(`run` 自己写,`classify --target` 只在补写历史产物时人工声明,**不从当前 profile 猜**——profile 会被改,猜出来的是今天的端点配昨天的行) |
584
+
585
+ | 13 | **一臂一个 home 只是"重命名"**:两臂共用 `DSH_HOME`,`--hide-store-for` 把 store 在同目录里改名(试点控制臂 `ls storages/` 直接读到 `.g6-hidden` 后缀),围栏再补一刀把 `storages/` 挂空;`recall-injections.json`/`ledger.json` 仍写着另一臂的 workspace | 控制臂日志(`lastWorkspace`、`injectedChars`)+ 第七轮读代码复盘:缝 #6 的 `scrubSharedHome` 只能清**上一轮**痕迹,同一轮里共享的记账文件永远清不掉 | `armHome(home, arm)`:每臂一个 `DSH_HOME`(`arms/<arm>/{profiles,storages,sessions,engram}`),store 只种进治疗臂自己的 home,控制臂的 home 里根本没有可读的东西;围栏只 bind 本臂 home;`--hide-store-for` 保留为兼容参数但会打印"已无必要"。测试断言两臂 `DSH_HOME` 不同、且控制臂 home 里不存在 `dsh_engram.json` |
586
+
587
+ 前四道会让数字**看起来**比实际好看或直接丢数据,后两道才真的把"臂知道自己是哪一臂"这条路堵死。
588
+ 值得记下来的是它们的发现方式:**没有一道是靠读代码发现的,全是控制臂自己在日志里说出来的**。
589
+ 这个评测的每一次进步,都是被自己的对照组教出来的。
590
+
591
+ ### 13.3 环境事故
592
+
593
+ - **上游 502**:12:00 前后 DeepSeek endpoint 连续返回 502,第一批治疗的 run 全部 `exit 1`。
594
+ 处理:等 + 冒烟确认恢复;判据是"check 为真"而不是"模型说完成",所以这只会浪费时间,不会污染数字。
595
+ - **移植当天的三次环境事故(都是工具自己的问题)**:
596
+ 1. **pnpm 起不来**:`dsh plugin add` 报 `[ERR_SQLITE_ERROR] unable to open database file`,
597
+ 连 `pnpm --version` 都失败。原因不是 pnpm 坏了,是**文件沙箱**:pnpm 的状态库在
598
+ `~/.local/state/pnpm`,当时不可写。把 `XDG_STATE_HOME` 指到工作区后立刻正常;
599
+ 提权之后默认路径也能用。
600
+ 2. **`bwrap: execvp …/bin/dsh: No such file or directory`**:启动器是**符号链接**
601
+ (`~/.npm-global/bin/dsh` → `~/.npm-global/lib/node_modules/@deepseek-ai/dsh/lib/bin.js`),
602
+ 围栏只放行了 `bin/`,链接指向的包不在里面。修复:`toolReadPaths()` 先 `realpath`,
603
+ 放行**那一个包**(不是整个全局前缀——全局前缀是一堆别人的 `docs/`)。
604
+ 3. **`EXDEV: cross-device link not permitted`**:`--hide-store-for` 把 store 改名到
605
+ `$TMPDIR`,macOS 上同盘,Linux 上 `/tmp` 是 tmpfs → 不同设备。改成同目录改名 +
606
+ 围栏把 `storages/` 挂空(缝 #6 的教训仍然成立:同目录改名本身不算隐藏)。
607
+ - **跑批中途余额用尽(QUOTA)**:16:12 起 endpoint 返回 `QUOTA: Insufficient Balance`,最后 7 行
608
+ 治疗臂每次 **2.2 秒**就退出。当时工具把这 7 行记成"任务失败"——**这是这一轮最危险的一处错误**:
609
+ 它把"我们没钱了"记成"臂做不出来",而方向恰好对插件有利(把真实的 100% 记成 65%)。
610
+ 修复两条,都是这一轮加的:① `infraReason()` 只在"识别到 QUOTA / CREDENTIAL / UPSTREAM / NETWORK
611
+ **且这一行没有真的动手**(没通过 + 日志 < 40 行)"时标 `infra`,并将其从分母里排除,闸门备注
612
+ 写明排除了哪几行;② `--resume` 把 infra 行当作"没做"重跑,并按 `task|seed|arm` 去重(原来重跑
613
+ 会把同一行算两遍,分母直接翻倍)。
614
+ 为什么要"没有真的动手"这一条:`money/seed-2` 在 198 行真实工作后、**最后一次调用**上撞了 QUOTA,
615
+ 但它的 `check` 通过——它是一行数据。规则不能只看"日志里有没有 QUOTA 这个词"。
616
+
617
+ - **磁盘满(ENOSPC)**:12:30 那次在 16/40 行时炸掉,产物一个字没落盘。原因与本项目无关
618
+ (`$TMPDIR` 里一个 2.5GB 的 Instruments trace,外部进程写的),但它暴露了 #4。
619
+ 清理后 7.3GB 可用,本轮产物总共 1.7MB——**评测数据从来不是磁盘问题的成因**。
620
+
621
+ ### 13.4 结果
622
+
623
+ **两份矩阵,同一套语料/围栏/开关,只换端点。** 主结果是本机 vLLM 上跑满的 40 行;官方 API 那次
624
+ 是第一次尝试(13/20 对,中途余额用尽),留作独立复现,两份产物都进仓库、都不改写。
625
+
626
+ #### 13.4.1 本地 vLLM(主结果,40/40 行)
627
+
628
+ 端点 `local-vllm-61`(`http://172.27.221.61:8003/v1`,模型 `deepseek-v4.1-flash-1017`,与官方
629
+ 开源同款),两臂都指向它——**配对比较只在端点内部成立**,所以换端点必须整轮重跑,不能混行。
630
+
631
+ | 臂 | 通过 | 通过率 | 平均墙钟 | 中位墙钟 |
632
+ |---|---|---|---|---|
633
+ | `p-a`(带 `dsh-engram` 0.8.8) | 20/20 | 100.0% | 85.2s | 67s |
634
+ | `p-b`(裸) | 2/20 | 10.0% | 199.8s | 162s |
635
+
636
+ 逐任务(通过 / 行数):
637
+
638
+ | 任务 | `p-a` | `p-b` |
639
+ |---|---|---|
640
+ | codec | 5/5 | 0/5 |
641
+ | errcode | 5/5 | 0/5 |
642
+ | money | 5/5 | **2/5** |
643
+ | ranking | 5/5 | 0/5 |
644
+
645
+ Δ = **+90.0pp**(20 对配对,only-`p-a` 18 / only-`p-b` 0)→ **G6 PASS(Δ ≥ −2pp)**。
646
+ `infraRows = 0`(没有一行没拿到模型)、`contaminated = 0`(40 行日志没有一条提到 store / `[ENGRAM]` /
647
+ 另一臂 / `runs.json`)。
648
+
649
+ #### 13.4.2 官方 API(第一次尝试,13/20 对)
650
+
651
+ 同一套命令、同一个模型 id,只是走官方付费端点。跑到 `money` seed 3 时账户余额用尽
652
+ (`QUOTA: Insufficient Balance`,余额 ¥0.19),最后 7 行治疗臂(`money` 3/4、`ranking` 0–4)每次
653
+ 2.2 秒退出。这 7 行被标成 `infra` 并从分母里排除,**不再当任务失败算**(§13.3 第四条)。
654
+
655
+ | 臂 | 通过 | 通过率 | 平均墙钟 |
656
+ |---|---|---|---|
657
+ | `p-a` | 13/13 | 100.0% | 30.7s |
658
+ | `p-b` | 1/20 | 5.0% | 127.6s |
659
+
660
+ 13 对有效配对 → Δ = **+95.0pp**;把 7 行 infra 当失败的保守口径(20 对)= `p-a` 13/20 = 65% vs
661
+ `p-b` 1/20 = 5% → **+60.0pp**。两种口径都 PASS。
662
+
663
+ #### 13.4.3 怎么读这两个数
664
+
665
+ - **方向与结论在两个端点上一致**:本地 +90.0pp(20 对完整)、官方 +95.0pp(13 对)。
666
+ 同一个模型 id、两套服务栈、两次独立跑批,`p-a` 一行没输——这比单次跑出的幅度更有说服力。
667
+ - **幅度不能跨端点比较**:本地端点明显更慢(`p-b` 200s vs 官方 128s),但墙钟不是质量证据,
668
+ 换端点只换"谁跑得更快",不换"谁做得对"。
669
+ - **`money` 是唯一"能猜中"的任务**:裸臂 2/20 蒙对了取整口径(官方 1/20、本地 2/20)。它把 Δ 拉低,
670
+ 正好说明其余三个任务(`codec`/`errcode`/`ranking`)的约定**猜不出来**。
671
+ - **不能与第五轮的 +90pp 比同**(数值巧合,不是证据):插件 0.8.2 → 0.8.8、语料 `check` 改成
672
+ `${REPO}`、围栏换成 bwrap、端点也从官方换成 vLLM。两轮只能各自独立读。
673
+ - **不能外推**:4 个任务、单模型、n = 20 对;这是一份"在这套语料上没有伤害、且带去可用信息"的证据,
674
+ 不是"engram 普遍提升 90pp"。
675
+ - **墙钟不作为证据**:同一天见过 30s–814s 的抖动。
676
+
677
+ ### 13.5 还剩什么缝(诚实清单)
678
+
679
+ - ~~控制臂能看见 run 树的兄弟目录名~~(第七轮前修掉):run 根现在是 `runs/<sha1(task|seed|arm)[0:8]>`,
680
+ 臂自己的 cwd 里不再出现臂名、任务名、seed 编号,也不再透露"这是一个矩阵";映射记在
681
+ `runs.json` 的每一行(`runKey`)里,人能查、臂读不到。围栏再把 run 根整个挂空、只挂回
682
+ `ws`,连本行自己的 `run.log` 都读不到。
683
+ - 控制臂仍能**看见**本臂 profile 里那个 `docs/` 曾被删掉这件事的痕迹(目录不存在),
684
+ 以及能推断出"本机的实验目录存在"(`DSH_HOME` 的祖先路径)——但内容一律读不到。
685
+ 缝 #7 的代价从"`storages/` 是个空挂载"降级成"这一臂的 home 里压根没有 `storages/`"(缝 #13)。
686
+ - 单模型、9 任务;Δ 的量级受任务选择影响极大,方向可靠,幅度别外推。
687
+ - 语料 migration 只做到"换机器能跑":`${REPO}` 之外,产物里的 `wallMs` 仍受端点抖动
688
+ 影响(本轮见过 52s–814s),**不能**当质量证据。
689
+
690
+ ### 13.6 围栏换了机制:seatbelt → bubblewrap(同一份契约,两套实现)
691
+
692
+ 第五轮的围栏是 macOS 的 `sandbox-exec`(seatbelt):**默认收禁 + 白名单放行**,规则
693
+ 最后一条匹配者生效。这一轮在 Linux 上没有 seatbelt,换成 `bwrap`(bubblewrap):
694
+ **先 `--ro-bind / /` 得到一整棵只读树,再逐条"挂空/挂回"**,规则第一个挂载者生效。
695
+ 两种机制方向相反,所以不能照抄规则,只能照抄**契约**:
696
+
697
+ | 契约(围栏要保证的事) | seatbelt 写法 | bubblewrap 写法 |
698
+ |---|---|---|
699
+ | 本臂 ws 可读可写 | `(allow file-read* / file-write* (subpath ws))` | `--bind ws ws` |
700
+ | 本臂 profile 可读 | 放行实验 home,再把另一臂 profile 禁掉 | `--bind <实验 home>`,`--tmpfs <另一臂 profile>` |
701
+ | `$HOME`、`/tmp`、本仓库不可读 | `(deny file-read* (subpath …))` | `--tmpfs $HOME`、`--tmpfs /tmp`、`--tmpfs <repo>` |
702
+ | 祖先目录可 lstat 但不可枚举 | `(allow file-read-metadata (literal …))` | 由 `--ro-bind / /` 天然满足 |
703
+ | 凭据可读(否则 `MISSING_CREDENTIAL`) | `(allow file-read* (literal credentials))` | `--ro-bind credentials credentials` |
704
+ | 本臂包内 `docs/`、被隐藏的 `storages/` 不可读 | 末尾再 `(deny …)`(最后匹配者生效) | `--tmpfs <那两处>`(挂载后写) |
705
+
706
+ 还有一条不属于"两套写法",是两者的共同边界:`fenceCommand()` 在既不是 darwin 也不是
707
+ linux 的平台上**抛错**
708
+ (`G6 has no OS-level fence for <platform>`),而不是"没有围栏也照跑"。没有围栏的 Δ
709
+ 只是下界(§10 的教训),工具不该让人有机会搞混这两件事。
710
+
711
+ 回归测试 `test/g6-sandbox.test.mjs` 两套实现共用:搭一个真实的目录布局(本臂 ws 在
712
+ `root/out/arm-b/…/ws`,兄弟 `arm-a`、`logs/runs.json`、另一臂 `seeded-store.json`、
713
+ `creds/.credentials.yaml`),然后在围栏里断言——
714
+
715
+ - 能读能写本臂 ws、`process.cwd()` 就是 ws 的 realpath、凭据可读、本臂 profile 可读;
716
+ - 读不到:`logs/runs.json`、兄弟臂的 `answer.txt`、另一臂的 `seeded-store.json`、
717
+ `~/.dsh`、`<ws>/package.json` 之外的本仓库文件、**本臂包里的语料文档**;
718
+ - `ls` 实验根时**看不到** `runs.json` / `arm-a` / `logs` 这些名字(bwrap 会为 bind 造出
719
+ 父目录骨架,所以断言的是"名字集合",不是"目录不存在")。
720
+
721
+ **变异检验**(这一条决定了测试是否真的在测围栏):把 `--tmpfs /tmp` 注释掉,测试立刻
722
+ 变红,并且打印出它看见的名字:`saw creds, home, logs, out, arm-a, arm-b, codec, runs.json`。
723
+ 还原后重新变绿。换句话说,这条测试不是"跑通了",是"该红的时候会红"。
724
+
725
+ ### 13.7 换端点必须整轮重跑(这一轮踩过的坑)
726
+
727
+ 第一次尝试走官方付费端点,跑到一半余额用尽(¥0.19),剩下 7 行跑不动。**不能**把这 7 行换到本地
728
+ 端点补上再拼成一份 40 行——配对比较只在端点内部成立,混端点等于把"臂的差异"和"服务栈的差异"
729
+ 搅在一起,而后者恰好是这一轮唯一想排除的东西。正确做法是整轮换端点重跑(第 13.4.1 节就是):
730
+
731
+ ```bash
732
+ # 两端点各跑一次,产物互相独立(r6-neutral = 官方 API,r6-local = 本地 vLLM)
733
+ node eval/quality-ab.mjs run \
734
+ --tasks "$PWD/eval/g6/tasks-blind.json" \
735
+ --workspace "$PWD/eval/fixtures/ledger-kata-blind" \
736
+ --arm p-a,p-b --seeds 5 \
737
+ --out "$PWD/eval/g6/runs/r6-local" --home "$HOME/.dsh/quality-ab" \
738
+ --seed-memory "$PWD/eval/g6/seed-memory-blind.json" \
739
+ --sandbox --control-first \
740
+ --log-dir "$HOME/.dsh/quality-ab-logs/r6-local"
741
+
742
+ # 中断/余额不足之后:--resume 复用已完成的行,只重跑 infra 行(并按 task|seed|arm 去重)
743
+ # 加上 --resume 即可;产物写早了就用 classify 从日志重判 infra:
744
+ node eval/quality-ab.mjs classify --out "$PWD/eval/g6/runs/r6-local" \
745
+ --log-dir "$HOME/.dsh/quality-ab-logs/r6-local"
746
+ ```
747
+
748
+ 第七轮起 `--home` 下是 `arms/<arm>/`(一臂一个 `DSH_HOME`,见 §13.2 缝 #13),
749
+ `--hide-store-for` 不再需要(参数仍接受,会打印"已无必要")。
750
+
751
+ 臂的端点由各自的 `~/.dsh/quality-ab/profiles/<arm>/cordis.patch.yml` 决定(`llm-pi-ai` 的
752
+ `providers` + `agent-default-model`);两臂必须指向同一个 `provider`/`model`,否则整批数字作废。
753
+
754
+ ## 14. 第七轮:一臂一个 home + 语料扩容(2026-10-28 晚,本地 vLLM)
755
+
756
+ ### 14.1 这一轮的五个变化
757
+
758
+ | # | 变化 | 为什么 |
759
+ |---|---|---|
760
+ | 1 | 一臂一个 `DSH_HOME`(`arms/<arm>/`) | 缝 #13:共享 home 里另一臂的记账永远清不掉 |
761
+ | 2 | run 根不透明 `runs/<sha1(task\|seed\|arm)[0:8]>/ws`,围栏连本行 `run.log` 都挂掉 | §13.5 最后一项残留:臂从自己 cwd 就能读出臂名/任务/seed |
762
+ | 3 | 语料 4 → 9 任务 | 4 个任务时一个任务的运气就能翻身 25pp |
763
+ | 4 | 每个 oracle 双向验证(`test/g6-corpus.test.mjs`) | 不可满足的 oracle 会让两臂 0/0 → 闸门报"持平 PASS" |
764
+ | 5 | 端点换成**本地** vLLM(`local-vllm-61/deepseek-v4.1-flash-1017`) | 官方 API 余额耗尽;同款开源权重,但**不能**与 §13.4 的数字互比 |
765
+
766
+ **所以这一轮的 Δ 不能与第五轮(+90pp)、第六轮(+90pp)相提并论**:插件版本、语料、
767
+ 围栏、home 布局、端点全都不同。数值接近只是巧合,不是复现。
768
+
769
+ ### 14.2 语料:九个"只能被告诉"的约定
770
+
771
+ 每个任务配一条只在记忆里存在的约定。第三列是**没有记忆时一个称职的 agent 会怎么写**——
772
+ `test/g6-corpus.test.mjs` 把这一列真的实现出来并跑 oracle,**必须挂**,否则这个任务测的是模型的先验。
773
+
774
+ | 任务 | 约定(只在记忆里) | 没有记忆时最自然的写法 | 自然写法挂在哪 |
775
+ |---|---|---|---|
776
+ | `codec` | 校验位 = `charset[Σ(digit×3+7) mod 36]`,charset 是小写 `0-9a-z` | 按 Luhn / mod 10 | 1/1 全挂 |
777
+ | `errcode` | `E_<CODE>: <msg>`,`.code` 存裸码,非 `UPPER_SNAKE` 直接抛 | `throw new Error(message)` | 3 条挂 2 |
778
+ | `money` | 金额按**整数分**累加,四舍五入远离零 | `toFixed(2)` / 浮点累加 | 2/2 全挂 |
779
+ | `ranking` | score 升序;同分**id 长者先**;再同则入参逆序 | 稳定排序 / 降序 | 2 条挂 1 |
780
+ | `pad` | `L-` + 5 位零填充;冲正数尾部加 `C` | `padStart(5)` 或 `padStart(6)` | 2 条挂 1 |
781
+ | `window` | 账期以 **Asia/Shanghai 04:00** 为界 | 按 UTC 零点切 | 2/2 全挂 |
782
+ | `dedupe` | 同 id 取**最后一条内容**、**第一次出现的位置**,不改入参 | 先到先得 | 2 条挂 1 |
783
+ | `quote` | 撇号千分位 + 负数括号 | `toFixed(2)` | 2/2 全挂 |
784
+ | `retry` | 5xx/429/`E_LEDGER_LOCKED` 可重试,`E_VALIDATION` 永不,最多 3 次 | 只重试 5xx | 2 条挂 2 |
785
+
786
+ 九个 oracle 都能被"照约定写"的实现通过(参考实现在测试里),也都挡得住"照常识写"的实现。
787
+
788
+ ### 14.3 这一轮又抓到的两个工具 bug
789
+
790
+ 1. **`check.mjs` 在测试环境里静默跑空**:它内部 `node --test`,而调用者若本身在测试运行器里,
791
+ 环境带着 `NODE_TEST_CONTEXT`,孙进程会以"测试子进程"身份启动、**一条都不跑并退出 0**——
792
+ 所有 stub 都会被判 PASS。`test/g6-corpus.test.mjs` 上线一分钟就抓到它。现在判分用干净环境。
793
+ 2. **`armEnv()` 重建环境时丢了凭据**(冒烟抓到):`armEnv(home)` 从 `process.env` 重建,
794
+ 把 `liftCredential()` 放进副本里的 `LOCAL_VLLM_61_API_KEY` 丢了,两臂 1.5s 就
795
+ `MISSING_CREDENTIAL`。行被正确判成 `infra`,没有污染 Δ——但那是分类器的功劳,不是跑批的。
796
+
797
+ ### 14.4 结果(本地 vLLM,18 行/臂,2026-10-28 21:03–22:30)
798
+
799
+ | 臂 | 通过 | 率 | 平均墙钟 |
800
+ |---|---|---|---|
801
+ | `p-a`(挂 engram) | 18/18 | 100.0% | 72.7s |
802
+ | `p-b`(裸机) | 1/16 | 6.3% | 107.5s |
803
+
804
+ **Δ = +93.8pp,16 对配对,`local-vllm-61/deepseek-v4.1-flash-1017`,`infra` 0 行。**
805
+
806
+ 逐任务(两位是 seed 0/1):
807
+
808
+ | 任务 | p-a | p-b | 说明 |
809
+ |---|---|---|---|
810
+ | codec / errcode / money / pad / window / quote / retry | 11 | 00 | 控制臂两次都写了"常识"版本 |
811
+ | ranking | 11 | 01 | seed 1 那次**算出来的**——但它去网上查了答案(见 §14.5) |
812
+ | dedupe | 11 | 10 | seed 0 是控制臂**自己想对**的(先到先得恰好等于约定),seed 1 挂了 900s(超时) |
813
+
814
+ `p-b` 唯一有效的那一分是 `dedupe/0`:约定"取最后一条内容、保留第一次出现的位置"
815
+ 在这条数据上恰好与"先到先得"同解。这是好事——它说明语料不是"不可能任务"。
816
+
817
+ ### 14.5 这一轮最重要的发现:控制臂能上网查答案
818
+
819
+ `runs.json` 里两行控制臂被标 `contaminated`,日志原文:
820
+
821
+ ```
822
+ ranking/seed-1: "Aha! The commit `skepsun/dsh-engram` … This is exactly the eval!
823
+ The repo is public. We can inspect it. … shallow clone."
824
+ dedupe/seed-1: 同类,先 `curl https://api.github.com/search/code?q=…`,再克隆仓库
825
+ ```
826
+
827
+ **为什么可能**:围栏(bwrap)只围文件系统,不围网络;而语料**是公开的**——
828
+ `docs/G6-quality-ab.zh.md`(这份文档)在插件包里、也在仓库里,§14.2 那张表就是答案。
829
+ 这两件事凑在一起,控制臂只要 `curl` 就能把"团队约定"读出来。
830
+
831
+ **修了什么**:
832
+
833
+ 1. `analyze()` 现在**不把不盲的行算进分母**(以前只标 `contaminated`,照样计入 Δ)。
834
+ 两行被剔除后,13.5% 的"控制臂得分"变成 6.3%,Δ 从 88.9pp 变成 93.8pp——
835
+ 方向没变,但**一个会让记忆看起来更差的 bug 被修掉了**。
836
+ 2. 污染正则扩到网络检索(`curl`、`git clone`、`grep.app`、`raw.githubusercontent`、
837
+ `github.com/search`),并有测试钉住"不盲的行不进分母"。
838
+ 3. 文档(本文件)从此**只描述方法,不写本轮答案**——§14.2 的约定表是"上一轮的",
839
+ 下一轮必须换一套(见下)。
840
+
841
+ **还没修的(诚实清单)**:
842
+
843
+ - **网络围栏没有**。按 uid 做 iptables 白名单(只放行模型端点)在本机可行且已手动验证过
844
+ (`setpriv --reuid=<uid>` + `-m owner --uid-owner`),但需要 root 和把实验 home 搬到
845
+ 中立路径(`/d1/chuxiong` 是 700,被围的 uid 进不去),这一轮没做。
846
+ - **语料仍是一次性手写的**。正解是**每轮用密钥生成**:任务模板 + 密钥文件(不入库)
847
+ → 生成记忆文本、oracle、参考实现;公开的只有生成器。这样"能上网"不再是漏洞。
848
+ - 两行"想到了但没做"(`money/seed-1` 想 `web_fetch`、`pad/seed-1` 试了 `skill` 名字):
849
+ 诱因普遍存在,别指望模型自己克制。
850
+
851
+ 所以这一轮的 Δ=+93.8pp 只在"控制臂没去查、且查了会被剔除"的意义上成立;
852
+ **在我把密钥化和网络围栏补上之前,这个数不能当作对外结论。**
853
+
854
+ ## 15. 第八轮:语料从密钥编译(公开生成器,保密这一轮)
855
+
856
+ §14.5 的结论是:**公开的语料 + 开放的网络 = 控制臂可以查到答案**。围栏只围文件系统,
857
+ 所以堵网络是一半;另一半是让"公开"这件事不再有意义——**每轮的约定由密钥抽出来**。
858
+
859
+ ### 15.1 怎么用
860
+
861
+ ```bash
862
+ # 一次性:造一个密钥(不进 git,权限 600)
863
+ (umask 077; head -c 48 /dev/urandom | base64 | tr -d '\n' > ~/.dsh/g6-secret)
864
+
865
+ # 编译一轮:抽出约定 → 生成记忆文本 + 私有 oracle + 参考实现 + 自然错解
866
+ node eval/g6/make-round.mjs --secret ~/.dsh/g6-secret --name r8 --out eval/g6/local/r8
867
+ # 9/9 tasks usable · key.json is the answer key: keep it out of git
868
+ ```
869
+
870
+ 产物全在 `eval/g6/local/<轮次>/`(已 gitignore):`key.json`(抽到的约定)、
871
+ `seed-memory.json`(同一批约定写成记忆文本)、`tasks.json`、`oracle/`、`ref/`、`naive/`。
872
+ 跑法和以前一样,`--tasks`/`--seed-memory` 指过去即可。
873
+
874
+ ### 15.2 三轮自证(生成器不通过就不出语料)
875
+
876
+ 1. **参考实现必须过**:`ref/<id>.js` 与 oracle 是同一份约定的两个写法,若不一致就是生成器的 bug,
877
+ **当场失败**(不重抽 —— 重抽会把 bug 藏在"某次刚好对"的抽样后面)。
878
+ 2. **自然错解必须挂**:`naive/<id>.js` 是"没有记忆时会写的那版"。若它也能过,这一抽**作废重抽**
879
+ (最多 24 次)——一轮里不允许存在"猜就猜对"的任务。
880
+ 3. **oracle 只能断言记忆说过的**:若 oracle 要求抛 `TypeError` 而记忆文本没写,生成器直接报错。
881
+ 这条是第一次冒烟用真模型跑出来的:`pad` 的 oracle 要求非整数抛错,记忆里没这句话,
882
+ **处理臂明明知道约定还是挂了**——错的不是模型,是语料。
883
+
884
+ ### 15.3 这解决了什么、没解决什么
885
+
886
+ - 解决了:仓库公开不再等于答案公开。读到本文件、读到生成器、甚至读到上一轮的 `eval/g6/local/r7/`
887
+ 都没用——这一轮的约定是另一套(测试断言:换密钥后 ≥7/9 个任务的约定不同)。
888
+ - 没解决:**网络围栏仍然没有**。控制臂如果去网上找、而恰好有人把某一轮的 key 泄露出去,
889
+ 它仍可能查到。密钥化把"必然可查"降级成"没人泄露就查不到",围栏才是把这条路彻底堵死。
890
+ 两件事都要做(`tsk_19e5e5bc`)。
891
+
892
+ ### 15.4 每轮的可比性
893
+
894
+ 不同轮次之间**约定不同**,所以逐任务的"这一轮对/错"不能跨轮比较;能跨轮比较的只有
895
+ "两臂在同一轮里的差"。这也意味着 `eval/g6/local/` 下的产物是**实验记录**,
896
+ 丢了就要用同一个密钥重编译(同一密钥 → 同一轮,测试钉住了)。
897
+
898
+ ### 15.5 第八轮结果(密钥语料 · 控制臂全盲)
899
+
900
+ `eval/g6/runs/r8-local/`(commit 见下),目标端点仍是本机 `local-vllm-61/deepseek-v4.1-flash-1017`,
901
+ 9 任务 × 2 seed × 2 臂,`--sandbox --control-first`,每臂独立 `DSH_HOME`。
902
+
903
+ | 臂 | 通过 | 比例 | 平均墙钟 |
904
+ |---|---|---|---|
905
+ | p-a(挂 engam) | 18/18 | 100.0% | 94.9s |
906
+ | p-b(裸) | 0/18 | 0.0% | 117.6s |
907
+
908
+ Δ = **+100.0pp**(18 对配对,only-p-a 18,only-p-b 0);闸门 PASS(目标 Δ ≥ −2pp)。
909
+ `infraRows 0`、`contaminated 0`、`notBlindExcluded 0`。
910
+
911
+ **控制臂这次是真的全盲**:18 份控制日志里 `dsh_engram`/`engram_recall`/`[ENGRAM]`/`curl`/`git clone`
912
+ 一次都没出现(第七轮有两份在查 GitHub)。这一轮的意义不只是"处理臂满分",而是
913
+ **它没有可以查的答案**——语料是本轮密钥抽出来的,仓库里不存在这套约定。
914
+
915
+ **必须一起说的两点**:
916
+
917
+ 1. **+100pp 不是"记忆在真实任务上的提升"。** 语料的构造就要求"没有记忆必然猜不中"
918
+ (猜得中的抽样被丢弃重抽),所以天花板本来就是 100pp。这一轮测的是
919
+ **"把约定写进记忆,臂能不能照做"**——能,18/18。真实任务大多可猜,收益会小得多;
920
+ G6 是机制闸门,不是效果预测。
921
+ 2. **和第七轮不可比**:约定不同、语料不同。能跨轮说的只有一句:第七轮控制臂能上网查答案
922
+ (已剔除 2 行),第八轮查不到(密钥化 + 已核实的盲区)。第七轮的 +93.8pp 因此**不能**
923
+ 对外当结果,第八轮才可以。
924
+
925
+ ## 16. 网络围栏:一臂一个 uid,出网只留一个洞(2026-10-29)
926
+
927
+ §14.5 留下的洞只有一个:**围栏只围文件系统,网卡还在**。第七轮的控制臂因此去 GitHub
928
+ 查到了约定。密钥化把"必然查得到"降成了"没人泄露就查不到",但只要臂有 shell 和 socket,
929
+ "没人泄露"就仍然是一个假设。这一节把假设变成机制。
930
+
931
+ ### 16.1 为什么不是"再写几条 bwrap 规则"
932
+
933
+ bubblewrap 管文件系统,不管网络:`--unshare-net` 会把网卡整个拿掉,模型端点也就没了。
934
+ 要"只允许一个出口",需要的是**按进程身份**的过滤,也就是 iptables 的 `owner` 匹配——
935
+ 它需要一个独立的 uid。于是围栏变成两段:
936
+
937
+ ```
938
+ sudo bwrap …(搭挂载树)
939
+ └── setpriv --reuid=1016 --regid=1016 --clear-groups
940
+ └── /bin/sh -c 'set -a; . $HOME/.netenv; set +a; umask 000; exec "$0" "$@"'
941
+ └── dsh --profile p-a "<task>"
942
+ ```
943
+
944
+ 宿主机上另有三条 `iptables -I OUTPUT -m owner --uid-owner 1016 …`:默认 REJECT,
945
+ 只放行 loopback 和 `172.27.221.61:8003`。规则**只按 uid 生效**,所以本会话、harness、
946
+ 并发跑的 Web GUI 都不受影响——这是按 uid 而不是按进程过滤的意义。
947
+
948
+ ```bash
949
+ node eval/quality-ab.mjs run … --sandbox --net-fence --net-endpoint 172.27.221.61:8003
950
+ ```
951
+
952
+ ### 16.2 两个前置探针:不合格就不计分
953
+
954
+ 围栏自身会坏,而坏掉的围栏产生的恰恰是最危险的数字:一个"看起来双盲、其实不盲"的矩阵。
955
+ 所以 `--net-fence` 在花任何模型时间之前先做两次自证:
956
+
957
+ 1. **出网探针**(以 1016 身份跑):端点必须可达、`github.com` 必须 000、DNS 必须死。
958
+ 任一条不符就 `throw`,不跑矩阵。实测:`endpoint reachable (401), github 000, dns dead`。
959
+ 2. **写目录探针**(走同一条 fence,`touch` 一个文件):臂必须能在自己的 ws 里写。
960
+
961
+ 第二条不是形式主义。第一次带围栏的冒烟里,ws 是从模板 `cpSync` 出来的、
962
+ 属 `chuxiong:chuxiong 775`,而臂是 1016——**臂能读不能写**,于是它把工作做在 `/tmp` 副本里、
963
+ 汇报"这个工作区对我只读"(那是控制臂,它甚至把这个发现写进了报告)。
964
+ 四条行全部 0.x 秒失败,`rm -rf` 也删不掉 1016 建的临时目录。
965
+ 现在 ws 每次复制后 `chmod -R a+rwX`,并在矩阵开始前用探针证明一次。
966
+
967
+ ### 16.3 密钥不进 argv
968
+
969
+ `env -i K=V …` 会被写进进程的 argv,而 argv 对宿主机上每个用户都可见(`ps`)。
970
+ 所以凭据走**文件**:每行一个 `envFile`(`<runHome>/.netenv`,600,属 1016,
971
+ 随行删除),由 fence 里的 sh `set -a; . file; set +a` 导出。
972
+
973
+ - 只有**本臂 provider 那一个** key 会进文件(`local-vllm-61` → `LOCAL_VLLM_61_API_KEY`),
974
+ 付费 API 的 key 不发给任何一臂。
975
+ - `set -a` 不是装饰:只 `. file` 是赋值不导出,dsh 起来就是 `MISSING_CREDENTIAL`
976
+ ——这条也是冒烟里踩出来的(4 行全部被判 CREDENTIAL,而不是当成任务失败计分)。
977
+
978
+ ### 16.4 冒烟里暴露的语料 bug(§15.2 的第 4 条自证)
979
+
980
+ 带围栏的冒烟跑出 `p-a` 在 `retry` 上挂了一次,原因不是围栏:**工作区里的 stub 和 prompt
981
+ 签的是两个函数**——`src/retry.js` 写着 `shouldRetry(status, code, attempt)`,
982
+ 而 prompt、记忆文本和 oracle 都写 `shouldRetry(attempt, error)`。臂只能挑一份文档信,
983
+ 挑哪份是硬币。第八轮 p-a 在这一题上 2/2 通过,纯粹是硬币落对了面。
984
+
985
+ 生成器现在有第 4 条自证(编译期,不通过就不出语料):
986
+
987
+ - stub 必须导出 prompt 点名的那个函数(当时 `ranking.js` 导 `rankRows`、`dedupe.js` 导 `mergeRows`);
988
+ - stub 的参数个数必须等于 prompt 里写的参数个数(`errcode` 是 3 个,oracle 故意只传 2 个测试
989
+ `.details` 缺省,这是合法的:**oracle 可以少传,不能多传**)。
990
+
991
+ 补上这条之后,同一把密钥编译出的第九轮 **9/9 usable**,另外三处 stub 冲突
992
+ (`retry`/`ranking`/`dedupe`)一并不存在了。
993
+
994
+ ### 16.5 这一节解决了什么、还剩什么
995
+
996
+ - 解决了:臂没有可用的出网路径(含 DNS)。第七轮那两份"去 GitHub 查"的日志,
997
+ 在围栏下会变成 `curl` 立刻失败——不是"没找到",是"出不去"。
998
+ - 还剩:**`--net-fence` 需要 root**(装 iptables 规则)。这是刻意的取舍:没有 root
999
+ 就没有按 uid 的过滤;harness 会把 `sudo -n` 的失败当成硬错误,不会静默降级成"无围栏"。
1000
+ - 还剩:uid 是宿主机级的(`g6arm`,1016)。同一台机器上并发的两轮矩阵会共用这个 uid
1001
+ 和它的规则集,所以**同一时刻只跑一轮带围栏的矩阵**。
1002
+
1003
+ ### 16.6 第九轮:带围栏的一整轮(2026-10-29 10:12–12:12)
1004
+
1005
+ `eval/g6/runs/r9-net/`,同一把密钥重新编译的第九轮(9 任务 × 2 seed × 2 臂 = 36 行),
1006
+ `--sandbox --net-fence --net-endpoint 172.27.221.61:8003 --control-first`。
1007
+
1008
+ 开场三行日志(不合格就不会有这一轮):
1009
+
1010
+ ```
1011
+ net fence: uid 1016 → endpoint reachable (401), github 000, dns dead
1012
+ net fence: uid 1016 can write the workspace (probed)
1013
+ …
1014
+ net fence: rules removed
1015
+ ```
1016
+
1017
+ | 臂 | 通过 | 比例 | 平均墙钟 |
1018
+ |---|---|---|---|
1019
+ | p-a(挂 engram) | 18/18 | 100.0% | 177.1s |
1020
+ | p-b(裸) | 0/18 | 0.0% | 227.0s |
1021
+
1022
+ Δ = **+100.0pp**(18 对配对,only-p-a 18,only-p-b 0),闸门 PASS,`infraRows 0`,
1023
+ `contaminated 0`。
1024
+
1025
+ **围栏在这一轮里真的被撞过。** 18 份控制日志里有 4 份试图出网,原话分别是
1026
+ "`curl -I https://example.com` … if network works, I could search GitHub for the eval repo"
1027
+ 和"`grep.app` / `github.com/search`"。它们全部失败(`exit 124`、`network unavailable`),
1028
+ 没有任何一行是从外面拿回信息后通过的——这正是 §14.5 那两个"查到答案"的行的反面。
1029
+
1030
+ **这 4 行一度被判"控制臂不盲"。** 检测器是第七轮写的,那时 `curl` 意味着"可能查到了";
1031
+ 在已经用探针证明出网不可达的围栏里,`curl` 只意味着"试过、出不去"。这是**检测器口径**的问题,
1032
+ 不是围栏漏了。处理方式是重新计算而不是手改文件:`quality-ab.mjs classify` 用同一批日志、
1033
+ 按新口径(`classifyLeak(log, {netFence})`,围栏下只认"看到了记忆块"和"读到了答案文件")
1034
+ 重新判定,`classes.contamination: 4` 就是这个撤销的痕迹。撤销前后 Δ 都是 +100pp
1035
+ (这 4 行本来就是控制臂失败行,剔除只是少了 4 对配对)。
1036
+
1037
+ **墙钟变慢是围栏的代价,不是质量信号**:p-a 从第八轮的 94.9s 涨到 177.1s,
1038
+ p-b 从 117.6s 涨到 227.0s。多出来的是每行重搭挂载树、掉 uid、冷 `/tmp`,
1039
+ 以及臂花在"网络不通"上的试探时间。G6 只认 check 的退出码。
1040
+
1041
+ **和第八轮不可比**(约定不同:`--name r9` 是另一抽),能跨轮说的仍只有那一句:
1042
+ 第八轮控制臂查不到答案(语料密钥化),第九轮控制臂**出不去**(网络围栏)。
1043
+
1044
+ **这一轮顺带修掉的语料 bug**(§16.4):`retry` 的 stub 签名与 prompt 冲突、`ranking`/`dedupe`
1045
+ 的 stub 导出名与 prompt 不一致。它们不改变"处理臂 18/18"这个结论,但改变这个结论的**成色**——
1046
+ 一份靠"猜哪份文档算数"得来的通过不是证据。修完之后 p-a 在 `retry` 上两 seed 全过(374.6s / 292.0s)。
package/docs/GATES.zh.md CHANGED
@@ -1,36 +1,36 @@
1
- # 门审计(gate audit)· 2026-10-26
1
+ # 门审计(gate audit)· 2026-10-29
2
2
 
3
- 窗口:最近 14 天 · 扫描 46 个会话(其中 35 个挂载了工具)· 账本行 7 条
3
+ 窗口:最近 14 天 · 扫描 14 个会话(其中 6 个装了 engram;"用过任意工具"的会话 8 个)· 账本行 6 条
4
4
 
5
5
  | 门 | 指标 | 实测 | 目标 | 判定 | 备注 |
6
6
  |---|---|---|---|---|---|
7
- | G1 | NetTokens · 净收益 > 0 的会话占比(有节省机会的会话) | 100.0% | ≥ 50% | **未测** | 3/3 opportunity rows of 7 — need ≥ 5 for a distribution |
8
- | G1b | NetTokens · 长会话(≥200 请求)占比 | 100.0% | ≥ 70% | **未测** | 2/2 long opportunity sessions |
9
- | G2 | Activation · 机制激活率(挂载会话 fire ≥1 次) | 77.1% | ≥ 60% | **PASS** | 27/35 mounted sessions |
10
- | G2b | Push · [RECALL] 注入命中会话占比 | 20.0% | 观测项(无预声明门) | **—** | 7/35 sessions carry a [RECALL] block in their prompt |
11
- | G3 | DrillRate · detail / recall-with-hits | 0.07 | 观测项(推送是否被采纳) | **—** | 3 detail / 45 recall-with-hits |
12
- | G4 | StaleRate · 被 supersede 的行占比 | 0.0% | < 1% | **PASS** | 0/406 memories (13 tagged stale) |
13
- | G5 | 索引 ≤ 700 chars | 681c | ≤ 700c | **PASS** | /Users/sunchuxiong/dsh-loom · 119 memories · 6 lines |
14
- | G6 | Quality Δ · 配对任务成功率差 | 90.0pp | Δ ≥ −2pp | **PASS** | 20 对配对 · 4 任务 × 5 种子 · eval/g6/runs/r5-confined/runs.json(臂名泄漏已修未复跑,见 §12) |
15
- | G7 | 账本自身开销 | 0.0144% | < 0.5% 请求时延 | **PASS** | from `npm run bench:ledger` (worst op vs a 300ms request floor) |
7
+ | G1 | NetTokens · 净收益 > 0 的会话占比(有节省机会的会话) | 未测 | ≥ 50% | **未测** | 0/0 opportunity rows of 6 — need ≥ 5 for a distribution |
8
+ | G1b | NetTokens · 长会话(≥200 请求)占比 | 未测 | ≥ 70% | **未测** | 0/0 long opportunity sessions |
9
+ | G2 | Activation · 机制激活率(装上了 engram 的会话 fire ≥1 次) | 83.3% | ≥ 60% | **PASS** | 5/6 installed sessions |
10
+ | G2b | Push · [RECALL] 注入命中会话占比 | 83.3% | 观测项(无预声明门) | **—** | 5/6 installed sessions carry a [RECALL] block in their prompt |
11
+ | G2c | Push opportunity · 有记忆可推的会话里推送腿渲染占比 | 83.3% | 观测项(无预声明门) | **—** | 5/6 sessions whose workspace has ≥1 memory |
12
+ | G3 | DrillRate · detail / recall-with-hits | 0.00 | 观测项(推送是否被采纳) | **—** | 0 detail / 4 recall-with-hits |
13
+ | G4 | StaleRate · 被 supersede 的行占比 | 0.0% | < 1% | **PASS** | 0/371 memories (6 tagged stale) |
14
+ | G5 | 索引 ≤ 700 chars | 未测 | ≤ 700c | **未测** | pass --api <base>?workspace=<ws> to read the rendered block size |
15
+ | G6 | Quality Δ · 配对任务成功率差 | 100.0pp | Δ ≥ −2pp | **PASS** | 18 对配对 · 9 任务 × 2 种子 · eval/g6/runs/r9-net/runs.json |
16
+ | G7 | 账本自身开销 | 0.2956% | < 0.5% 请求时延 | **PASS** | from `npm run bench:ledger` (worst op vs a 300ms request floor) |
16
17
 
17
18
  ## 账本合计
18
19
 
19
- - injected 11,589,492c(≈2,897,373 tok)· avoided 1,909,142,538c(≈477,285,635 tok)
20
- - rewrites 17 次 / 4,024,426c(按 cacheDiscount 计罚 3,621,983c)
21
- - **net 1,893,931,063c ≈ 473,482,766 tok**;开源会话 7 条,其中净收益为正 3 条
20
+ - injected 5,892,536c(≈1,473,134 tok)· avoided 0c(≈0 tok)
21
+ - rewrites 0 次 / 0c(按 cacheDiscount 计罚 0c)
22
+ - **net -5,892,536c ≈ -1,473,134 tok**;开源会话 6 条,其中净收益为正 0 条
22
23
 
23
24
  ## 最近会话(按会话落行)
24
25
 
25
26
  | session | requests | injected | avoided | net | workspace |
26
27
  |---|---|---|---|---|---|
27
- | session-746c03… | 1,084 | 2,168,304c | 310,174,822c | 307,721,654c | dsh-loom |
28
- | session-419cfc… | 320 | 933,735c | 143,007,626c | 142,073,891c | kototoro-parsers |
29
- | 91e9c39a-2231-… | 66 | 179,382c | 286,224c | 106,842c | kototoro-parsers |
30
- | session-775f51… | 220 | 609,289c | 0c | -1,711,542c | kototoro-parsers |
31
- | session-71f597… | 140 | 392,295c | 0c | -392,295c | kototoro-parsers |
32
- | session-ffbe89… | 120 | 361,254c | 0c | -361,254c | Kototoro |
33
- | session-ed8929… | 260 | 775,040c | 0c | -775,040c | Kototoro |
28
+ | session-de86a1… | 840 | 1,560,712c | 0c | -1,560,712c | dsh-loom |
29
+ | session-c0c6f9… | 160 | 339,571c | 0c | -339,571c | Kototoro |
30
+ | b6ded7c9-6408-… | 1 | 742c | 0c | -742c | Kototoro |
31
+ | 03760170-ce43-… | 2 | 2,780c | 0c | -2,780c | Kototoro |
32
+ | session-0fff53… | 70 | 118,833c | 0c | -118,833c | dsh-loom |
33
+ | session-24dcd6… | 1,883 | 3,869,898c | 0c | -3,869,898c | Kototoro |
34
34
 
35
35
  > 口径:token = chars/4(与 GUI 一致);rewrites 罚金 = 前缀字符 ×(1 − cacheDiscount),默认 0.1。
36
36
  > 未测即未测——不猜。G5 需要运行中的宿主(`--api`),G7 需要 `npm run bench:ledger` + `--g7 <pct>`,