dsh-engram 0.8.8 → 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 +3 -2
- package/README.zh.md +2 -1
- package/docs/G6-quality-ab.zh.md +506 -0
- package/docs/GATES.zh.md +21 -21
- package/docs/HANDOFF-2026-10-28.md +246 -0
- package/docs/RELEASE-NOTES-v0.9.0.md +78 -0
- package/eval/gate-audit.mjs +119 -15
- package/package.json +1 -1
- package/scripts/dsh-engram.mjs +27 -16
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.
|
|
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
|
|
|
@@ -295,7 +295,8 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
|
|
|
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
297
|
| `0.8.7` (npm) | published | 同 `0.5.0` — 推送腿落盘(`$DSH_HOME/engram/push-legs.jsonl`)+ `status` 打印事件日志读法;`minimal` 预设不再承诺永远不会发生的自动接管 | 同 `0.5.0` |
|
|
298
|
-
| `0.8.8`
|
|
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` |
|
|
299
300
|
|
|
300
301
|
### Where the API family broke — `0.1.2-alpha.2`
|
|
301
302
|
|
package/README.zh.md
CHANGED
|
@@ -190,7 +190,8 @@ curl -s -X POST http://127.0.0.1:3080/api/dsh-engram/import \
|
|
|
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
192
|
| `0.8.7`(npm) | 已发布 | 同 `0.5.0` —— 推送腿落盘(`$DSH_HOME/engram/push-legs.jsonl`)+ `status` 打印事件日志读法;`minimal` 预设不再承诺永远不会发生的自动接管 | 同 `0.5.0` |
|
|
193
|
-
| `0.8.8
|
|
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` |
|
|
194
195
|
|
|
195
196
|
### API 族断裂点:`0.1.2-alpha.2`
|
|
196
197
|
|
package/docs/G6-quality-ab.zh.md
CHANGED
|
@@ -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-
|
|
1
|
+
# 门审计(gate audit)· 2026-10-29
|
|
2
2
|
|
|
3
|
-
窗口:最近 14 天 · 扫描
|
|
3
|
+
窗口:最近 14 天 · 扫描 14 个会话(其中 6 个装了 engram;"用过任意工具"的会话 8 个)· 账本行 6 条
|
|
4
4
|
|
|
5
5
|
| 门 | 指标 | 实测 | 目标 | 判定 | 备注 |
|
|
6
6
|
|---|---|---|---|---|---|
|
|
7
|
-
| G1 | NetTokens · 净收益 > 0 的会话占比(有节省机会的会话) |
|
|
8
|
-
| G1b | NetTokens · 长会话(≥200 请求)占比 |
|
|
9
|
-
| G2 | Activation ·
|
|
10
|
-
| G2b | Push · [RECALL] 注入命中会话占比 |
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
15
|
-
|
|
|
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
|
|
20
|
-
- rewrites
|
|
21
|
-
- **net
|
|
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-
|
|
28
|
-
| session-
|
|
29
|
-
|
|
|
30
|
-
|
|
|
31
|
-
| session-
|
|
32
|
-
| session-
|
|
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>`,
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# 交接 · G6 第六轮(2026-10-28 · 已跑完)
|
|
2
|
+
|
|
3
|
+
> 阅读顺序:本文 → `docs/G6-quality-ab.zh.md` §13(方法/十二道缝/四次事故/两份结果)→ `eval/quality-ab.mjs`。
|
|
4
|
+
|
|
5
|
+
## 0. 一句话
|
|
6
|
+
|
|
7
|
+
**第六轮跑完了:40/40 行,20 对配对,Δ = +90.0pp,G6 PASS,污染 0 行。**
|
|
8
|
+
`p-a`(带 `dsh-engram` 0.8.8)20/20,`p-b`(裸)2/20。跑在本机 vLLM 端点
|
|
9
|
+
(`local-vllm-61` / `deepseek-v4.1-flash-1017`,与官方开源同款),**两臂同端点**。
|
|
10
|
+
|
|
11
|
+
第一次尝试走的是官方付费端点,跑到 13/20 对时账户余额用尽(`QUOTA`,¥0.19),那 7 行按 `infra`
|
|
12
|
+
排除、不当失败算。那份产物(`eval/g6/runs/r6-neutral`,+95.0pp)留在仓库里当**独立复现**:
|
|
13
|
+
同一个模型 id、两套服务栈、两次跑批,方向一致、`p-a` 一对没输。
|
|
14
|
+
|
|
15
|
+
详见 `docs/G6-quality-ab.zh.md` §13.4(两份结果怎么读)、§13.3(四次事故)、§13.6(围栏移植)、
|
|
16
|
+
§13.7(换端点必须整轮重跑)。**下一轮别再用付费端点热身**。
|
|
17
|
+
|
|
18
|
+
## 1. 今天在仓库里留下了什么
|
|
19
|
+
|
|
20
|
+
| commit | 一句话 | 为什么必须有 |
|
|
21
|
+
|---|---|---|
|
|
22
|
+
| `3f62aa9` | 种子键用 `realpath` 后的写法 | 不修的话**治疗臂根本没被种上记忆**(`--out` 写 `/tmp/…`,插件按 `getcwd()` 记 `/private/tmp/…`),两臂跑出来一样,Δ≈0 还看不出来 |
|
|
23
|
+
| `573cb64` | `--resume` 真的能复用 | 原来 resume 块有 TDZ 异常被 `catch` 吞掉,打印成"没有可续的行",整批重跑 |
|
|
24
|
+
| `bf65c4b` | 每行原子落盘 + 空盘预检 | 第六轮第一次在 16/40 行时 ENOSPC,**1.7 小时蒸发**;现在最坏也只丢当前那一行 |
|
|
25
|
+
| `4b8ce6f` | 祖先目录只给 `file-read-metadata`;在跑产物搬到 `--log-dir` | 原来臂能 `ls` 实验根,看到 `runs.json`/`arm-a`/`arm-b`,10/20 的控制会话据此推理"这是评测矩阵" |
|
|
26
|
+
| `e510929` | 矩阵开始前 scrub 共享 home | 两臂共用 `DSH_HOME`,`recall-injections.json` 写着另一臂的 workspace,`ledger.json` 按路径记 `injectedChars`,7/7 控制日志引用了 `profiles/ab-engram` |
|
|
27
|
+
| (文档) | `docs/G6-quality-ab.zh.md` §13:方法、十二道缝、四次事故、§13.4 两份结果 | 每个结论都指到证据;两份产物分别对应当钱/免费两个端点 |
|
|
28
|
+
| `371be60` | G2 分母改成"装了 engram 的会话";run 根改成 `runs/<sha1[0:8]>` | 旧分母把 3 个没装插件的会话算进激活率(50% 假 FAIL);run 路径里写着臂名/任务名/seed |
|
|
29
|
+
| `a27e237` | 一臂一个 `DSH_HOME`;语料 4 → 9 任务 | 共享 home 是结构性的泄漏(`scrubSharedHome` 只能清上一轮);5 个新任务都要求"只能被告知"的约定 |
|
|
30
|
+
| `26d143d` | 语料自检:每个 oracle 双向验证 + 任务与记忆对齐 | 没有这道检查,一个不可满足的 oracle 会让两臂都 0/0,闸门报"两臂持平 PASS";它当场抓出 `check.mjs` 在测试环境里静默跑空 |
|
|
31
|
+
|
|
32
|
+
测试:`npm test` → 508/508(含 `test/quality-ab` 18 条、`test/g6-sandbox` 4 条、`test/gate-audit` 6 条、
|
|
33
|
+
`test/g6-corpus` 2 条)。
|
|
34
|
+
(后者是新加的围栏回归:node 能在围栏里启动、ws 可读、实验根不可枚举、`runs.json` 与另一臂打不开;
|
|
35
|
+
把规则改回 `file-read*` 它会红)。
|
|
36
|
+
|
|
37
|
+
## 2. 停的时候的状态(本机,不随仓库走)
|
|
38
|
+
|
|
39
|
+
| 东西 | 位置 | 是否迁移 |
|
|
40
|
+
|---|---|---|
|
|
41
|
+
| 已跑的 10 行 | `eval/g6/runs/r6-partial/runs.json`(已提交) | ✅ 带走 |
|
|
42
|
+
| 那 10 行的 run.log / check 输出 | `/tmp/g6/r6g-logs/`(596K) | ❌ 不迁移,需要证据就把需要的几份捞出来 |
|
|
43
|
+
| 10 个 workspace | `/tmp/g6r16/`(260K) | ❌ 不迁移 |
|
|
44
|
+
| 实验 home | `~/.dsh/quality-ab/`:profile `p-a`(装了 engram **0.8.2**)/ `p-b`(裸) | ❌ 新机器重建(§3.3) |
|
|
45
|
+
|
|
46
|
+
停之前没有任何后台进程在跑(`pgrep -f "quality-ab.mjs run"` 为空)。
|
|
47
|
+
|
|
48
|
+
## 3. 在另一台机器上继续
|
|
49
|
+
|
|
50
|
+
### 3.1 前置
|
|
51
|
+
|
|
52
|
+
- **macOS**。围栏用的是 `sandbox-exec`/seatbelt;Linux 得换 bwrap/firejail,工具里没有这条路。
|
|
53
|
+
- node 22(`node -p process.version`),dsh 0.2.x(`dsh --version`)。
|
|
54
|
+
- `~/.dsh/.credentials.yaml` 里有 `DEEPSEEK_API_KEY`(harness 自己读进子进程环境,不落日志)。
|
|
55
|
+
|
|
56
|
+
### 3.2 代码与自检
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
git pull && npm install
|
|
60
|
+
node --test test/quality-ab.test.mjs test/g6-sandbox.test.mjs # 11 个,应全绿
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### 3.3 建两臂(**中性名**,不要再用 `ab-engram`/`ab-bare`)
|
|
64
|
+
|
|
65
|
+
**一臂一个 `DSH_HOME`**(2026-10-28 起,缝 #13):每臂的 home 是 `--home` 下的 `arms/<arm>/`,
|
|
66
|
+
插件必须装进**本臂**的 home——装到上一层会造出一个没人用的第三个 profile,治疗臂悄悄变成裸的,
|
|
67
|
+
Δ 塌成 0,而闸门会报"两臂持平 PASS"。
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
H=$HOME/.dsh/quality-ab-r7
|
|
71
|
+
node eval/quality-ab.mjs prepare --arm on=p-a,off=p-b --home "$H"
|
|
72
|
+
DSH_HOME="$H/arms/p-a" dsh plugin --profile p-a add "$PWD/dsh-engram-8.8.tgz" # 版本按 §3.3 的决定
|
|
73
|
+
cp ~/.dsh/quality-ab/profiles/p-a/cordis.patch.yml "$H/arms/p-a/profiles/p-a/" # 端点/模型
|
|
74
|
+
cp ~/.dsh/quality-ab/profiles/p-b/cordis.patch.yml "$H/arms/p-b/profiles/p-b/"
|
|
75
|
+
DSH_HOME="$H/arms/p-a" dsh --profile p-a --dump-config | grep -A2 'id: engram' # 有
|
|
76
|
+
DSH_HOME="$H/arms/p-b" dsh --profile p-b --dump-config | grep -c engram # 应为 0
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**要先做的一个决定:插件版本。**
|
|
80
|
+
|
|
81
|
+
- 本机 `p-a` 里是 **0.8.2**(从第五轮的 `ab-engram` 整个拷过来的),仓库当前是 **0.8.8**。
|
|
82
|
+
- 想和第五轮(Δ=+90pp)**可比** → 装 0.8.2。
|
|
83
|
+
- 想让这轮数字**给 v0.8 门背书** → 装 0.8.8,并在 §13.4 写明"与第五轮不是同一版本,不可直接比"。
|
|
84
|
+
- 我的建议是后者(门存在的意义是给当前版本出数),但**无论选哪个都要写进 §13.4**。
|
|
85
|
+
|
|
86
|
+
### 3.4 冒烟(约 2 分钟,别省)
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
node eval/quality-ab.mjs run \
|
|
90
|
+
--tasks <只含 codec 一件任务的 tasks.json> \
|
|
91
|
+
--workspace eval/fixtures/ledger-kata-blind \
|
|
92
|
+
--arm p-a,p-b --seeds 1 \
|
|
93
|
+
--out /tmp/g6-smoke --home "$HOME/.dsh/quality-ab-r7" \
|
|
94
|
+
--seed-memory eval/g6/seed-memory-blind.json \
|
|
95
|
+
--sandbox --control-first --log-dir /tmp/g6-smoke-logs
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
期望:`p-b` fail、`p-a` PASS(本机今天 13:15 的冒烟正是这个结果,80.6s vs 187.6s)。
|
|
99
|
+
**若 `p-a` 也 fail,先怀疑种子键**(`3f62aa9`):看 `p-a` 的 run.log 里有没有出现 `[seed-0x]`;
|
|
100
|
+
没有就是记忆没种上,别往下跑。
|
|
101
|
+
|
|
102
|
+
### 3.5 跑批
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
# 第七轮(2026-10-28 起):9 任务 × 2 臂 × 2 seed = 18 对,本地 vLLM 端点
|
|
106
|
+
node eval/quality-ab.mjs run \
|
|
107
|
+
--tasks "$PWD/eval/g6/tasks-blind.json" \
|
|
108
|
+
--workspace "$PWD/eval/fixtures/ledger-kata-blind" \
|
|
109
|
+
--arm p-a,p-b --seeds 2 \
|
|
110
|
+
--out "$PWD/eval/g6/runs/r7-local" \
|
|
111
|
+
--home "$HOME/.dsh/quality-ab-r7" \
|
|
112
|
+
--seed-memory "$PWD/eval/g6/seed-memory-blind.json" \
|
|
113
|
+
--sandbox --control-first --min-pairs 10 \
|
|
114
|
+
--log-dir "$HOME/.dsh/quality-ab-logs/r7-local" \
|
|
115
|
+
--resume # 断了就加这条,复用已完成的行
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
注意 `--log-dir` 会接住**中途**的 `runs.json`(臂读不到它;`--out` 在围栏里被挂空,
|
|
119
|
+
中途写在那儿就等于把答案放在臂旁边),跑完的最终产物仍落在 `--out/runs.json` + `REPORT.md`。
|
|
120
|
+
|
|
121
|
+
- **想复用已跑的 10 行**:把 `eval/g6/runs/r6-partial/` 里的 `runs.json` 拷到 `--out` 目录,加 `--resume`。
|
|
122
|
+
注意那 10 行是"插件 0.8.2 + 本机环境"下跑出来的,混进 0.8.8 的一批里**不是同一个实验**;
|
|
123
|
+
除非你决定沿用 0.8.2,否则**建议丢掉、整批重跑**(这就是我把它放进 `r6-partial` 而不是 `r6-neutral` 的原因)。
|
|
124
|
+
- 单 run 今天 2–8 分钟(endpoint 波动很大),40 run ≈ **3–4 小时**。
|
|
125
|
+
- 中途断了直接加 `--resume` 重跑同一条命令:现在每行都落盘,会打印"复用了 N 行"。
|
|
126
|
+
- 磁盘 < 512MB 时工具会拒绝启动(`AB_MIN_FREE_MB` 可调)。今天那次 ENOSPC 与本项目无关
|
|
127
|
+
(`$TMPDIR` 里一个外部进程写的 2.5GB Instruments trace),整轮产物只有 1.7MB。
|
|
128
|
+
|
|
129
|
+
### 3.6 出数与填文档
|
|
130
|
+
|
|
131
|
+
跑完命令自己会打印 Δ 与门判定(预注册留一法 `Δ ≥ −2pp`,配对下限 10)。要填的:
|
|
132
|
+
|
|
133
|
+
1. `docs/G6-quality-ab.zh.md` **§13.4**:表(`p-a` / `p-b` 的 pass 与平均耗时)+ 逐任务控制臂 pass 数
|
|
134
|
+
+ "能说什么/不能说"(照 §12 的写法,别外推幅度)。
|
|
135
|
+
2. `docs/GATES.zh.md` 的 G6 行:
|
|
136
|
+
|
|
137
|
+
```bash
|
|
138
|
+
npm run bench:ledger # 拿 G7 的数字
|
|
139
|
+
node eval/gate-audit.mjs --days 14 --md --out docs/GATES.zh.md \
|
|
140
|
+
--g6 eval/g6/runs/r6-neutral/runs.json --g7 <bench 输出里那个百分比> \
|
|
141
|
+
--api 'http://127.0.0.1:3080/api/dsh-engram?workspace=<repo 绝对路径>'
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`--api` 只是为了让 G5(渲染块大小)能算;没有宿主就省掉 G5。
|
|
145
|
+
|
|
146
|
+
### 3.7 收尾
|
|
147
|
+
|
|
148
|
+
- ESR 任务 `tsk_81`("G6 第六轮:中性臂名复跑 20 对配对")现在是 ACTIVE、三证全缺;
|
|
149
|
+
跑完用 artifact(`eval/g6/runs/r6-neutral/runs.json`)+ evaluation(Δ 与门判定)+ memory_ref 关它。
|
|
150
|
+
- 存一条 memory:本轮 Δ、版本、以及"六个缝"里新学到的那条。
|
|
151
|
+
|
|
152
|
+
## 4. 这个评测工具长什么样(只需记住三件事)
|
|
153
|
+
|
|
154
|
+
1. **判据是命令不是模型**:每个任务一条 `check`(bash 退出码),不引入第二个模型打分。
|
|
155
|
+
2. **两臂在同一台机上跑,靠 seatbelt 隔离**:`--sandbox` 默认收禁 `/tmp` 和 `~`,只放行本臂 ws、
|
|
156
|
+
本臂 profile、node、凭据;另一臂 profile 再禁一次;`--control-first` 保证控制臂先跑完
|
|
157
|
+
(后跑的控制臂能读到治疗臂的 run.log,第二轮真发生过)。
|
|
158
|
+
3. **臂名不能出现在臂能看见的任何地方**:目录叫 `arm-a`/`arm-b`,profile 叫 `p-a`/`p-b`,
|
|
159
|
+
映射只写在 artifact 的 `armLabels` 里。今天补的六道缝都是为了这一条。
|
|
160
|
+
|
|
161
|
+
## 5. 未决 / 已知坑
|
|
162
|
+
|
|
163
|
+
1. **插件版本没对齐**(§3.3)——接手第一件事。
|
|
164
|
+
2. ~~共享 home 是结构性的~~ **已解决**(commit `a27e237`,缝 #13):一臂一个 `DSH_HOME`
|
|
165
|
+
(`arms/<arm>/{profiles,storages,sessions,engram}`),store 只种进治疗臂自己的 home,
|
|
166
|
+
控制臂的 home 里没有可读的东西;围栏只 bind 本臂 home。`--hide-store-for` 退化为兼容参数。
|
|
167
|
+
测试:两臂 `DSH_HOME` 不同、控制臂 home 里不存在 `dsh_engram.json`。
|
|
168
|
+
3. ~~控制臂仍能看见兄弟目录名~~ **已解决**(commit `371be60`):run 根改为
|
|
169
|
+
`runs/<sha1(task|seed|arm)[0:8]>/ws`,围栏再把 run 根挂空、只挂回 `ws`(连本行 `run.log`
|
|
170
|
+
都读不到);映射记在 `runs.json` 每行的 `runKey` 里。剩下的可见面只有"本机实验目录存在"。
|
|
171
|
+
|
|
172
|
+
4. **耗时不可控**:同一任务今天从 53s 到 400s 都有,endpoint 抖动是主因,别用耗时差异下结论。
|
|
173
|
+
5. `eval/g6/runs/` 下有历史 artifact(`r5-confined` = 第五轮 +90pp,带臂名泄漏),
|
|
174
|
+
别拿它跟第六轮混着算。
|
|
175
|
+
|
|
176
|
+
## 6. 证据索引
|
|
177
|
+
|
|
178
|
+
| 要看的 | 在哪 |
|
|
179
|
+
|---|---|
|
|
180
|
+
| 第六轮的方法/事故/两份结果 | `docs/G6-quality-ab.zh.md` §13(结果在 §13.4) |
|
|
181
|
+
| 第一至五轮 | 同文件 §4–§12 |
|
|
182
|
+
| 门表 | `docs/GATES.zh.md` |
|
|
183
|
+
| harness | `eval/quality-ab.mjs`(`scrubSharedHome` / `artifactPath` / `assertFreeSpace` / `snapshotRows`) |
|
|
184
|
+
| 围栏 | `eval/g6/sandbox-profile.mjs` |
|
|
185
|
+
| 测试 | `test/quality-ab.test.mjs`、`test/g6-sandbox.test.mjs` |
|
|
186
|
+
| 第六轮已跑的 10 行 | `eval/g6/runs/r6-partial/runs.json` |
|
|
187
|
+
|
|
188
|
+
## 7. Linux 接管记录(2026-10-28 下午)
|
|
189
|
+
|
|
190
|
+
上一台 macOS 上的东西一件没带过来,全部在本机重建。**结果(本地 vLLM,40/40 行)**:
|
|
191
|
+
|
|
192
|
+
| 臂 | 通过 | 通过率 | 平均墙钟 | 中位 |
|
|
193
|
+
|---|---|---|---|---|
|
|
194
|
+
| `p-a`(`dsh-engram` 0.8.8) | 20/20 | 100% | 85s | 67s |
|
|
195
|
+
| `p-b`(裸) | 2/20 | 10% | 200s | 162s |
|
|
196
|
+
|
|
197
|
+
逐任务:`codec` 5–0 · `errcode` 5–0 · `money` 5–2 · `ranking` 5–0 → **Δ = +90.0pp → G6 PASS**,
|
|
198
|
+
`infra 0` · `contaminated 0`。
|
|
199
|
+
|
|
200
|
+
官方端点的第一次尝试(`r6-neutral`):13 对有效配对、**+95.0pp**,7 行 `infra`(余额用尽);
|
|
201
|
+
保守口径(20 对,7 行当失败)**+60.0pp**。三种读法都 PASS。
|
|
202
|
+
|
|
203
|
+
### 7.1 与 macOS 轮次的差异(读数字前必须知道)
|
|
204
|
+
|
|
205
|
+
| 项 | macOS(第五轮) | 本机(第六轮) |
|
|
206
|
+
|---|---|---|
|
|
207
|
+
| 围栏 | `sandbox-exec`(seatbelt,DENY 列表) | `bwrap`(挂载列表),`fenceCommand()` 按平台选,**未知平台抛错** |
|
|
208
|
+
| 端点 | 官方付费 API | `local-vllm-61`(本机 vLLM,同款模型 id);`assertSameTarget()` 保证两臂一致 |
|
|
209
|
+
| 插件版本 | 0.8.2 | **0.8.8** |
|
|
210
|
+
| 语料 `check` | 写死 `/Users/sunchuxiong/...` | `node ${REPO}/eval/g6/check.mjs …`(拒绝没有占位符的写法) |
|
|
211
|
+
| 实验 home | `~/.dsh/quality-ab` | 同(本机重建,`p-a` 装 0.8.8,`p-b` 裸) |
|
|
212
|
+
| 日志 | `/tmp/g6/r6g-logs` | `~/.dsh/quality-ab-logs/r6-local`(在 home 之外、对臂不可读) |
|
|
213
|
+
|
|
214
|
+
**所以第六轮与第五轮的 +90pp 不能比同**:版本、语料、围栏实现都变了。
|
|
215
|
+
|
|
216
|
+
### 7.2 这一轮补的五个漏洞(都在 §13.2 的 #7–#11)
|
|
217
|
+
|
|
218
|
+
12. **两臂可能指向不同端点**:`run` 开头用 `--dump-config` 比两臂的 `provider`/`model`,不同就拒绝开跑;产物记 `target`
|
|
219
|
+
|
|
220
|
+
7. 插件包自带 `docs/`(**原文含 codec 公式**)被装进治疗臂自己的 `node_modules` → 装完即删 + 围栏挂空
|
|
221
|
+
8. 语料 `check` 写死上一台机器的绝对路径 → 两臂全 0 → 闸门报"两臂持平 PASS"(静默假结论)→ `${REPO}` + 拒绝规则
|
|
222
|
+
9. 臂继承了调用者的 `DSH_PROFILE_DIR`/`DSH_SESSION_ID`/`DSH_SHELL` → `armEnv()` 只给 `DSH_HOME`
|
|
223
|
+
10. `--control-first` 的比较器永不返回 `-1` → 改成显式分区(顺序本身就是隔离)
|
|
224
|
+
11. 仓库只是"恰好"因为位于 `$HOME` 下被藏住 → 显式 `--tmpfs <repo>`
|
|
225
|
+
|
|
226
|
+
### 7.3 现在可以直接跑的命令
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
# 整轮重跑(换端点就必须整轮)
|
|
230
|
+
node eval/quality-ab.mjs run --tasks "$PWD/eval/g6/tasks-blind.json" \
|
|
231
|
+
--workspace "$PWD/eval/fixtures/ledger-kata-blind" --arm p-a,p-b --seeds 5 \
|
|
232
|
+
--out "$PWD/eval/g6/runs/r6-local" --home "$HOME/.dsh/quality-ab" \
|
|
233
|
+
--seed-memory "$PWD/eval/g6/seed-memory-blind.json" --hide-store-for p-b \
|
|
234
|
+
--sandbox --control-first --log-dir "$HOME/.dsh/quality-ab-logs/r6-local"
|
|
235
|
+
# (中途断了加 --resume:复用已完成的行,只重跑 infra 行)
|
|
236
|
+
|
|
237
|
+
### 7.4 本机状态(不随仓库走)
|
|
238
|
+
|
|
239
|
+
| 东西 | 位置 |
|
|
240
|
+
|---|---|
|
|
241
|
+
| 40 行原始日志(run.log + check 输出) | `~/.dsh/quality-ab-logs/r6-local/`(本地端点)· `~/.dsh/quality-ab-logs/r6/`(官方端点那次) |
|
|
242
|
+
| 冒烟证据(2 次) | `~/.dsh/quality-ab-logs/smoke{,2}/` |
|
|
243
|
+
| 实验 home(`p-a` 0.8.8 / `p-b` 裸) | `~/.dsh/quality-ab/` |
|
|
244
|
+
| 产物 | `eval/g6/runs/r6-local/{runs.json,REPORT.md}`(主结果)· `r6-neutral`(官方端点复现)——都已提交 |
|
|
245
|
+
|
|
246
|
+
`dsh-engram-0.8.8.tgz`(`npm pack` 的产物)在仓库根,被 `.gitignore` 忽略;重装插件需要它。
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# dsh-engram v0.9.0
|
|
2
|
+
|
|
3
|
+
**这一版几乎没改产品代码,改的是「我们凭什么说自己有用」——评测从「跑得动」变成了「作弊不了」。**
|
|
4
|
+
|
|
5
|
+
从 `0.8.8` 到这里共 22 个 commit,`lib/` 一行未动,唯一的用户可见改动是 CLI 的一处
|
|
6
|
+
preset 扫描优先级。剩下的全部落在 `eval/`、`test/` 和 `docs/`:把上一版还只能"声称"的
|
|
7
|
+
东西变成能复现的读数。
|
|
8
|
+
|
|
9
|
+
## 修复
|
|
10
|
+
|
|
11
|
+
- **`web-compaction` 的 preset 扫描不再被陈旧的 legacy 目录抢占**:`--presets-dir`
|
|
12
|
+
现在优先于一切。此前一个更早的 checkout 留在机器上时,它会先认领 preset id,
|
|
13
|
+
声明式 patch 文件按 id 被去重掉,于是 `status` 报的是 legacy 布局、
|
|
14
|
+
而声明式那份被显示成 `custom`。三条 Linux CLI 测试抓到了这个组合
|
|
15
|
+
(`scripts/dsh-engram.mjs`)。
|
|
16
|
+
|
|
17
|
+
## 评测基础设施(本版真正的内容)
|
|
18
|
+
|
|
19
|
+
- **G6 有了网络围栏**(`--sandbox --net-fence --net-endpoint host:port`):
|
|
20
|
+
bwrap 搭挂载树 → `setpriv` 掉到专用 uid → 三条按 uid 生效的 iptables 规则,
|
|
21
|
+
默认 REJECT,只放行 loopback 和一个模型端点。按 uid 而不是按进程过滤,
|
|
22
|
+
所以开发会话、harness、Web GUI 的网络不受影响。
|
|
23
|
+
跑矩阵前有两个自证,不通过就不计分:出网探针(端点可达 / `github 000` / DNS 死)
|
|
24
|
+
和写目录探针(臂必须能在自己的 workspace 里落盘)。
|
|
25
|
+
- **第九轮结果**:9 任务 × 2 seed × 2 臂,p-a 18/18 vs p-b 0/18,Δ **+100.0pp**,
|
|
26
|
+
18 对配对,`infraRows 0`,`contaminated 0`(`eval/g6/runs/r9-net/`)。
|
|
27
|
+
18 份控制日志里有 4 份试图出网,全部拿到 `exit 124` / "network unavailable"——
|
|
28
|
+
这是第七轮「控制臂去 GitHub 查到答案」的反面。
|
|
29
|
+
- **语料第 4 条自证**:工作区 stub 必须导出 prompt 点名的函数,参数个数必须与 prompt 一致
|
|
30
|
+
(oracle 可以少传、不能多传)。这条当场抓出三处不一致:`retry` 的 stub 签的是
|
|
31
|
+
`shouldRetry(status, code, attempt)`、`ranking`/`dedupe` 的 stub 导出名 prompt 从未提过。
|
|
32
|
+
一份靠"猜哪份文档算数"得来的通过不是证据。
|
|
33
|
+
- **污染判定改为三分类**(`classifyLeak` → `memory` / `artifact` / `network`),
|
|
34
|
+
并提供 `quality-ab.mjs classify` 从既有日志**重算**:围栏下"试过出网"不再等于"拿到了"。
|
|
35
|
+
- **G7 从「未测」变成实测**:账本自身开销最坏操作 **0.2956%**(闸门 < 0.5%),
|
|
36
|
+
1123 条真实 step 延迟,`npm run bench:ledger` 可复现。
|
|
37
|
+
|
|
38
|
+
## 其他
|
|
39
|
+
|
|
40
|
+
- 证据面汇总见 `docs/GATES.zh.md`:G2 激活率 83.3% PASS · G4 陈旧率 0.0% PASS ·
|
|
41
|
+
G6 Δ +100pp PASS · G7 0.2956% PASS · G1/G1b/G5 仍未测(未测即未测,不猜)。
|
|
42
|
+
- G6 方法、十二道缝与失效模式见 `docs/G6-quality-ab.zh.md` §16。
|
|
43
|
+
|
|
44
|
+
## 这一版**不是** 1.0,也请不要这样读
|
|
45
|
+
|
|
46
|
+
`0.9.0` 表示"证据链条够了",不表示"公共面稳定了"。三条至少还挡在 1.0 前面:
|
|
47
|
+
|
|
48
|
+
1. **宿主还是 rc**(peer `>=0.1.2-alpha.2 <0.3.0-0`,本机 `dsh 0.2.1-rc.2`):
|
|
49
|
+
1.0 承诺"不无故破坏",在 rc 宿主上做不到。
|
|
50
|
+
2. **旗舰收益未测**:账本 `avoided 0c`、净 **−1.46M token**、G1 未测——
|
|
51
|
+
因为省钱的两条路(`boundaryPrune`、`gcReplacesCompaction`)默认关闭。
|
|
52
|
+
一次开着的实测周(≥5 个 opportunity 会话)才能让 G1 第一次有读数。
|
|
53
|
+
3. **减法债**:14 个工具(目标 ≤10)、70 个配置键(面向用户目标 ≤12)、
|
|
54
|
+
client bundle 291KB(目标 <150KB);存储格式仍无 schemaVersion/迁移。
|
|
55
|
+
|
|
56
|
+
## DSH 版本支持
|
|
57
|
+
|
|
58
|
+
- **支持的 DSH 范围**:`>=0.1.2-alpha.2 <0.3.0-0`
|
|
59
|
+
- **peer 声明**:
|
|
60
|
+
- `@deepseek-ai/dsh-compaction`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
61
|
+
- `@deepseek-ai/dsh-compaction-basic`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
62
|
+
- `@deepseek-ai/dsh-llm`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
63
|
+
- `@deepseek-ai/dsh-settings`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
64
|
+
- `@deepseek-ai/dsh-storage-domain`: `>=0.1.2-alpha.2 <0.3.0-0`
|
|
65
|
+
- **验证版本**:`dsh 0.2.1-rc.2`(本机 CLI + web)
|
|
66
|
+
- **最后验证日期**:2026-10-29
|
|
67
|
+
- **安装方式已验证**:`dsh plugin --profile web add <path|dsh-engram>`
|
|
68
|
+
- **兼容性**:配置键与数据格式与 `0.8.8` 完全一致,**无需迁移**;无破坏性改动。
|
|
69
|
+
|
|
70
|
+
## 验证
|
|
71
|
+
|
|
72
|
+
```
|
|
73
|
+
npm test → 519/519
|
|
74
|
+
npm run typecheck → ok
|
|
75
|
+
npm run eval → BENCH PASS
|
|
76
|
+
npm run build:client → 无 diff(bundle 新鲜)
|
|
77
|
+
npm pack → 459KB / 94 files
|
|
78
|
+
```
|
package/eval/gate-audit.mjs
CHANGED
|
@@ -40,6 +40,9 @@ const LONG_SESSION_REQUESTS = 200;
|
|
|
40
40
|
* gate FAIL (or PASS). The gate says 未测 until there is a distribution. */
|
|
41
41
|
const MIN_SESSION_ROWS = 5;
|
|
42
42
|
const MIN_LONG_ROWS = 3; // §6: 长会话 = ≥200 请求
|
|
43
|
+
// G2's population floor: "installed sessions". Below this the rate is a coin
|
|
44
|
+
// flip dressed as a percentage.
|
|
45
|
+
const MIN_ACTIVATION_SESSIONS = 5;
|
|
43
46
|
const ENGRAM_TOOL_RE = /^(engram_|esr_)/;
|
|
44
47
|
|
|
45
48
|
function parseArgs(argv) {
|
|
@@ -55,6 +58,9 @@ function parseArgs(argv) {
|
|
|
55
58
|
else if (arg === "--g7") out.g7 = Number(argv[++i]);
|
|
56
59
|
else if (arg === "--g6") out.g6 = argv[++i] ?? null;
|
|
57
60
|
else if (arg === "--home") out.home = argv[++i] ?? null;
|
|
61
|
+
// Diagnostics: which sessions are in the denominator, and what each one
|
|
62
|
+
// actually did. A rate without its population is a number you cannot act on.
|
|
63
|
+
else if (arg === "--explain") out.explain = true;
|
|
58
64
|
}
|
|
59
65
|
return out;
|
|
60
66
|
}
|
|
@@ -200,6 +206,35 @@ function scanSessions({ root = sessionsRoot(), days = 14, now = Date.now() } = {
|
|
|
200
206
|
return sessions.sort((a, b) => b.mtime - a.mtime);
|
|
201
207
|
}
|
|
202
208
|
|
|
209
|
+
/**
|
|
210
|
+
* How many memories each workspace has, so a session can be asked the only
|
|
211
|
+
* question that makes push rate meaningful: was there anything to push?
|
|
212
|
+
*/
|
|
213
|
+
function countMemoriesByWorkspace(dshHome) {
|
|
214
|
+
const candidates = [
|
|
215
|
+
path.join(dshHome, "storages", "dsh_engram.json"),
|
|
216
|
+
path.join(dshHome, ".dsh", "storages", "dsh_engram.json"),
|
|
217
|
+
];
|
|
218
|
+
for (const file of candidates) {
|
|
219
|
+
let parsed;
|
|
220
|
+
try {
|
|
221
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
222
|
+
} catch {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
const table = parsed?.tables?.memories;
|
|
226
|
+
if (table === null || table === void 0 || typeof table !== "object") continue;
|
|
227
|
+
const counts = new Map();
|
|
228
|
+
for (const row of Object.values(table)) {
|
|
229
|
+
if (row === null || typeof row !== "object" || row.kind === "episode") continue;
|
|
230
|
+
if (typeof row.workspace !== "string" || row.workspace.length === 0) continue;
|
|
231
|
+
counts.set(row.workspace, (counts.get(row.workspace) ?? 0) + 1);
|
|
232
|
+
}
|
|
233
|
+
return counts;
|
|
234
|
+
}
|
|
235
|
+
return new Map();
|
|
236
|
+
}
|
|
237
|
+
|
|
203
238
|
/** Stale share over the memory table (read-only; the store stays the truth). */
|
|
204
239
|
function readMemoryShare(dshHome) {
|
|
205
240
|
const candidates = [
|
|
@@ -285,12 +320,29 @@ export function auditGates({ days = 14, now = Date.now(), home = void 0, api = n
|
|
|
285
320
|
const netShare = pct(positive.length, opportunityRows.length);
|
|
286
321
|
const longShare = pct(longPositive.length, longRows.length);
|
|
287
322
|
|
|
288
|
-
// ── G2 Activation:
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
323
|
+
// ── G2 Activation: installed sessions where any engram/esr tool fired
|
|
324
|
+
//
|
|
325
|
+
// "installed" used to be `tools.size > 0` — that is "the session ran SOME
|
|
326
|
+
// tool", which is true of every session and says nothing about engram. On this
|
|
327
|
+
// machine that put three Kototoro sessions in the denominator whose tool list
|
|
328
|
+
// (bash, read, edit… — no engram_*, no esr_*) shows the plugin was never
|
|
329
|
+
// loaded there: they could not fire, and counting them measured the install
|
|
330
|
+
// state of a workspace, not activation. Evidence of a live mechanism is one of:
|
|
331
|
+
// the prompt carried the [ENGRAM]/[RECALL] block, the ledger counted requests,
|
|
332
|
+
// or an engram tool was actually called.
|
|
333
|
+
const ledgerLive = new Set(rows.map((row) => row.sessionId));
|
|
334
|
+
const engramFired = (session) => [...session.tools.keys()].some((name) => ENGRAM_TOOL_RE.test(name));
|
|
335
|
+
const installed = sessions.filter((s) => s.pushSeen || ledgerLive.has(s.sessionId) || engramFired(s));
|
|
336
|
+
const activated = installed.filter(engramFired);
|
|
337
|
+
const pushFired = installed.filter((s) => s.pushSeen);
|
|
338
|
+
const memoryCounts = countMemoriesByWorkspace(dshHome);
|
|
339
|
+
// The opportunity axis: a session whose workspace HAS memories is a session
|
|
340
|
+
// where the push leg had something to say. Below that, "no push" is not evidence.
|
|
341
|
+
const hadSomethingToPush = installed.filter((s) => (memoryCounts.get(s.workspace) ?? 0) > 0);
|
|
342
|
+
const pushWherePossible = hadSomethingToPush.filter((s) => s.pushSeen);
|
|
343
|
+
const activation = pct(activated.length, installed.length);
|
|
344
|
+
const pushShare = pct(pushFired.length, installed.length);
|
|
345
|
+
const pushOpportunityShare = pct(pushWherePossible.length, hadSomethingToPush.length);
|
|
294
346
|
|
|
295
347
|
// ── G3 DrillRate: detail calls per recall-with-hits
|
|
296
348
|
const recallWithHits = sessions.reduce((sum, s) => sum + s.recallWithHits, 0);
|
|
@@ -302,7 +354,16 @@ export function auditGates({ days = 14, now = Date.now(), home = void 0, api = n
|
|
|
302
354
|
|
|
303
355
|
return {
|
|
304
356
|
auditedAt: now,
|
|
305
|
-
|
|
357
|
+
// "mounted" now means what the word says: engram was actually live in the
|
|
358
|
+
// session. The old count (sessions that ran any tool) is kept beside it so a
|
|
359
|
+
// reader can see how much of the population the fix removed.
|
|
360
|
+
window: {
|
|
361
|
+
days,
|
|
362
|
+
now,
|
|
363
|
+
sessions: sessions.length,
|
|
364
|
+
mounted: installed.length,
|
|
365
|
+
anyTool: sessions.filter((s) => s.tools.size > 0).length,
|
|
366
|
+
},
|
|
306
367
|
ledger: {
|
|
307
368
|
rows: rows.length,
|
|
308
369
|
opportunityRows: opportunityRows.length,
|
|
@@ -345,19 +406,35 @@ export function auditGates({ days = 14, now = Date.now(), home = void 0, api = n
|
|
|
345
406
|
},
|
|
346
407
|
{
|
|
347
408
|
id: "G2",
|
|
348
|
-
name: "Activation ·
|
|
409
|
+
name: "Activation · 机制激活率(装上了 engram 的会话 fire ≥1 次)",
|
|
349
410
|
value: fmtPct(activation),
|
|
350
411
|
target: "≥ 60%",
|
|
351
|
-
|
|
352
|
-
|
|
412
|
+
// Enough sessions to judge: below the floor the honest answer is 未测.
|
|
413
|
+
// n=3 would read 100% PASS and mean nothing.
|
|
414
|
+
verdict:
|
|
415
|
+
installed.length < MIN_ACTIVATION_SESSIONS ? "未测" : verdict(activation >= 60),
|
|
416
|
+
note:
|
|
417
|
+
installed.length < MIN_ACTIVATION_SESSIONS
|
|
418
|
+
? `${activated.length}/${installed.length} installed sessions — 样本不足(需 ≥ ${MIN_ACTIVATION_SESSIONS});旧口径 ${sessions.filter((s) => s.tools.size > 0).length} 个"有工具"会话里有 ${sessions.filter((s) => s.tools.size > 0 && !s.pushSeen && !ledgerLive.has(s.sessionId) && !engramFired(s)).length} 个根本没装 engram`
|
|
419
|
+
: `${activated.length}/${installed.length} installed sessions`,
|
|
353
420
|
},
|
|
354
421
|
{
|
|
355
422
|
id: "G2b",
|
|
356
423
|
name: "Push · [RECALL] 注入命中会话占比",
|
|
357
424
|
value: fmtPct(pushShare),
|
|
358
425
|
target: "观测项(无预声明门)",
|
|
359
|
-
verdict:
|
|
360
|
-
note: `${pushFired.length}/${
|
|
426
|
+
verdict: installed.length === 0 ? "未测" : "—",
|
|
427
|
+
note: `${pushFired.length}/${installed.length} installed sessions carry a [RECALL] block in their prompt`,
|
|
428
|
+
},
|
|
429
|
+
{
|
|
430
|
+
id: "G2c",
|
|
431
|
+
name: "Push opportunity · 有记忆可推的会话里推送腿渲染占比",
|
|
432
|
+
value: fmtPct(pushOpportunityShare),
|
|
433
|
+
target: "观测项(无预声明门)",
|
|
434
|
+
verdict: hadSomethingToPush.length === 0 ? "未测" : "—",
|
|
435
|
+
// The only push number that can be read as a miss: the workspace had
|
|
436
|
+
// memories and the block still never rendered.
|
|
437
|
+
note: `${pushWherePossible.length}/${hadSomethingToPush.length} sessions whose workspace has ≥1 memory`,
|
|
361
438
|
},
|
|
362
439
|
{
|
|
363
440
|
id: "G3",
|
|
@@ -393,7 +470,11 @@ export function auditGates({ days = 14, now = Date.now(), home = void 0, api = n
|
|
|
393
470
|
verdict: g6 === null ? "未测" : (g6.gate?.verdict ?? "未测"),
|
|
394
471
|
note: g6 === null
|
|
395
472
|
? "needs paired A/B runs (not derivable from logs) — pass --g6 <runs.json>"
|
|
396
|
-
: `${g6.paired?.pairs ?? 0} 对配对 · ${g6.tasks?.length ?? 0} 任务 × ${g6.seeds ?? 1} 种子 · ${g6.source ?? ""}`.trim()
|
|
473
|
+
: `${g6.paired?.pairs ?? 0} 对配对 · ${g6.tasks?.length ?? 0} 任务 × ${g6.seeds ?? 1} 种子 · ${g6.source ?? ""}`.trim() +
|
|
474
|
+
// A control row that went and read the answer key is not evidence.
|
|
475
|
+
// If the artifact dropped some, the number on this dashboard is only
|
|
476
|
+
// honest when the drop is visible next to it.
|
|
477
|
+
(g6.notBlindExcluded > 0 ? ` · 已剔除 ${g6.notBlindExcluded} 行不盲控制臂` : ""),
|
|
397
478
|
},
|
|
398
479
|
{
|
|
399
480
|
id: "G7",
|
|
@@ -475,7 +556,7 @@ function renderMarkdown(report) {
|
|
|
475
556
|
const out = [];
|
|
476
557
|
out.push(`# 门审计(gate audit)· ${new Date(report.auditedAt ?? Date.now()).toISOString().slice(0, 10)}`);
|
|
477
558
|
out.push("");
|
|
478
|
-
out.push(`窗口:最近 ${report.window.days} 天 · 扫描 ${report.window.sessions} 个会话(其中 ${report.window.mounted}
|
|
559
|
+
out.push(`窗口:最近 ${report.window.days} 天 · 扫描 ${report.window.sessions} 个会话(其中 ${report.window.mounted} 个装了 engram;"用过任意工具"的会话 ${report.window.anyTool} 个)· 账本行 ${report.ledger.rows} 条`);
|
|
479
560
|
out.push("");
|
|
480
561
|
out.push("| 门 | 指标 | 实测 | 目标 | 判定 | 备注 |");
|
|
481
562
|
out.push("|---|---|---|---|---|---|");
|
|
@@ -524,7 +605,30 @@ async function main() {
|
|
|
524
605
|
return;
|
|
525
606
|
}
|
|
526
607
|
|
|
527
|
-
|
|
608
|
+
if (args.explain) {
|
|
609
|
+
const sessions = scanSessions({ days: args.days, root: sessionsRoot() });
|
|
610
|
+
const byId = new Map(ledger.sessions.map((row) => [row.sessionId, row]));
|
|
611
|
+
console.log("session ws req push tools engram-tools recall/hit detail all-tools");
|
|
612
|
+
for (const s of sessions) {
|
|
613
|
+
const row = byId.get(s.sessionId) ?? {};
|
|
614
|
+
const engramTools = [...s.tools.keys()].filter((name) => ENGRAM_TOOL_RE.test(name));
|
|
615
|
+
console.log(
|
|
616
|
+
[
|
|
617
|
+
s.sessionId.slice(0, 8).padEnd(12),
|
|
618
|
+
path.basename(s.workspace || "?").slice(0, 20).padEnd(22),
|
|
619
|
+
String(row.requests ?? 0).padStart(3),
|
|
620
|
+
String(s.pushSeen ? "yes" : "-").padStart(5),
|
|
621
|
+
String(s.tools.size).padStart(6),
|
|
622
|
+
(engramTools.join(",") || "-").slice(0, 18).padEnd(18),
|
|
623
|
+
([...s.tools.keys()].join(",") || "-").slice(0, 46).padEnd(46),
|
|
624
|
+
`${s.recallQueries}/${s.recallWithHits}`.padStart(11),
|
|
625
|
+
String(s.detailCalls).padStart(6),
|
|
626
|
+
].join(" "),
|
|
627
|
+
);
|
|
628
|
+
}
|
|
629
|
+
console.log("");
|
|
630
|
+
}
|
|
631
|
+
console.log(`gate audit · last ${report.window.days}d · ${report.window.sessions} sessions (${report.window.mounted} with engram live, ${report.window.anyTool} with any tool) · ${report.ledger.rows} ledger rows`);
|
|
528
632
|
for (const gate of report.gates) {
|
|
529
633
|
console.log(` ${gate.id.padEnd(4)} ${gate.verdict.padEnd(4)} ${gate.value.padEnd(8)} ≤/≥ ${gate.target.padEnd(10)} ${gate.name}`);
|
|
530
634
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-engram",
|
|
3
3
|
"description": "Minimalist long-term memory for DeepSeek Harness, distilling symbolic-index + pi-esr ideas: zero-LLM auto-capture, a symbolic [ENGRAM] index with progressive disclosure, and an ESR-lite evidence-closure protocol (esr_task / esr_node / esr_close / esr_link). Real agent-behaviour telemetry (usage rollup + /stats) and a deterministic offline recall benchmark (npm run eval). Hot path is model-free; storage on ctx.storageDomain; web memory viewer, observability panel and config card through DSH's native settings slots.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.9.0",
|
|
5
5
|
"packageManager": "pnpm@11.22.0",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"repository": {
|
package/scripts/dsh-engram.mjs
CHANGED
|
@@ -118,39 +118,50 @@ async function discoverPresets(status, presetsDirFlag) {
|
|
|
118
118
|
push(id, entry.path);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
// 2.
|
|
122
|
-
|
|
121
|
+
// 2. An explicit --presets-dir wins over everything: the operator named it.
|
|
122
|
+
// Without this, a stale legacy composed-preset root (this machine has an
|
|
123
|
+
// older checkout beside the repo) claims the ids first, the declarative
|
|
124
|
+
// patch files are deduped away by id, and `status` reports the legacy
|
|
125
|
+
// layout while showing the declarative ones as "custom" (three CLI tests
|
|
126
|
+
// caught exactly this on Linux).
|
|
127
|
+
const scanDeclarative = async (dir) => {
|
|
123
128
|
let entries = [];
|
|
124
129
|
try {
|
|
125
|
-
entries = await readdir(
|
|
130
|
+
entries = await readdir(dir, { withFileTypes: true });
|
|
126
131
|
} catch {
|
|
127
|
-
|
|
132
|
+
return;
|
|
128
133
|
}
|
|
129
134
|
for (const entry of entries) {
|
|
130
|
-
if (!entry.
|
|
131
|
-
|
|
132
|
-
if (file === undefined) continue;
|
|
133
|
-
push(entry.name, file);
|
|
135
|
+
if (!entry.isFile() || !entry.name.endsWith(".patch.yml")) continue;
|
|
136
|
+
push(entry.name.slice(0, -".patch.yml".length), join(dir, entry.name));
|
|
134
137
|
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
//
|
|
138
|
+
};
|
|
139
|
+
//
|
|
140
|
+
// Resolved once, up front: an explicit --presets-dir or DSH_WEB_PRESETS_DIR is
|
|
141
|
+
// the operator naming a directory, and the dsh-anchored default is the layout
|
|
142
|
+
// this DSH ships. Either beats a stale legacy root that only happens to exist.
|
|
138
143
|
const presetsDir = discoverPresetsDir({
|
|
139
144
|
presetsDir: presetsDirFlag,
|
|
140
145
|
anchors: [dshBinAnchor()].filter((anchor) => typeof anchor === "string"),
|
|
141
146
|
});
|
|
142
|
-
if (presetsDir !== undefined)
|
|
147
|
+
if (presetsDir !== undefined) await scanDeclarative(presetsDir);
|
|
148
|
+
|
|
149
|
+
// 3. Legacy roots.
|
|
150
|
+
for (const root of legacyPresetRoots()) {
|
|
143
151
|
let entries = [];
|
|
144
152
|
try {
|
|
145
|
-
entries = await readdir(
|
|
153
|
+
entries = await readdir(root, { withFileTypes: true });
|
|
146
154
|
} catch {
|
|
147
|
-
|
|
155
|
+
continue;
|
|
148
156
|
}
|
|
149
157
|
for (const entry of entries) {
|
|
150
|
-
if (!entry.
|
|
151
|
-
|
|
158
|
+
if (!entry.isDirectory()) continue;
|
|
159
|
+
const file = await compositionIn(join(root, entry.name));
|
|
160
|
+
if (file === undefined) continue;
|
|
161
|
+
push(entry.name, file);
|
|
152
162
|
}
|
|
153
163
|
}
|
|
164
|
+
|
|
154
165
|
return presets;
|
|
155
166
|
}
|
|
156
167
|
|