Commit Graph

7 Commits

Author SHA1 Message Date
e2c2bb41b7 docs: 删掉 README「nothing to install」假声明,改正 peer 叙述
d144284 补了「Prerequisite: link the harness packages first」章节,但留下两处
与新章节自相矛盾的话,这一提交收掉:

1. README §Install 的安装注释 `npm install  # nothing to install; dsh packages
   are peers` —— 直接删。实测它不成立:干净副本里 npm install 只拉到 cordis
   (四个 peer 里唯一非 optional 的)加它的传递依赖,dsh-llm /
   dsh-system-prompt / dsh-tools 三个仍然缺;不 install 更彻底,四个全缺。
   改成说明 peer 归属 + 指向前置章节的链接。
2. 新章节开头 `This repository has **no runtime dependencies**` —— 同样不成立
   (lib/index.js 运行时 import 三个 @deepseek-ai 包)。改成「不以本插件自身
   依赖的形式拉取任何东西」,把 peer 归属说清,并写明 npm install 只解决
   cordis 一个。
3. 症状段补一句 npm run check 同样红(实测 exit=1,checkpoints.js 在 import
   阶段就 ERR_MODULE_NOT_FOUND),原文只提了 npm test。
4. scripts/link-dsh.sh 头部注释里的 `zero runtime dependencies` 一并改成同义
   但不撒谎的说法。

验证(自己跑,不转述)
- 干净副本 git archive HEAD + 本补丁,不 link:
    not ok loader / phase-b-g / phase-b-h / plugin
    # tests 34  # pass 30  # fail 4
    npm run check exit=1(Cannot find package '@deepseek-ai/cordis')
- 同一副本 bash scripts/link-dsh.sh 之后:
    # tests 120  # pass 120  # fail 0
    npm run check exit=0,10 条 ✔
- npm install 单独一步(不 link)也复现:四文件 ERR_MODULE_NOT_FOUND,
  缺的是 dsh-system-prompt / dsh-llm(cordis 已装)
- README 全文再无「nothing to install / no runtime dependencies / zero
  runtime dependencies」类断言(grep 零命中)
- README 计数禁令仍绿,无 .orig/.rej/.bak 入库

更正一处旧结论:交接说「不 link 时 4 个文件 ERR_MODULE_NOT_FOUND」= 少 4 个
用例,不对。那四个文件在 import 阶段就死,用例根本没注册,所以 npm test 汇总
是 34 tests,不是 116/120。已在 README 里写成「a handful of tests rather than
the full suite」,避免再误导。
2026-10-30 15:07:45 +08:00
0e65dfee64 fix(check): checkpoint 全废时 runner 仍报通过 + 补回丢失的覆盖
缺口(评审复现,我独立复现确认)
上一条替换旧 README 判据时,把它的第二条断言一起带走了。
旧 test 同时守两件事:
  1) README 计数与实现一致      已判定不值得守,删
  2) checkpoints.js 至少注册一条 值得守,跟着 1 一起消失
grep 全仓:没有任何东西守 2。

实测后果(很安静)
把 helper 换成空操作、调用点全保留:
  npm run check  ->  0/0 checkpoints passed, exit=0
  npm test       ->  120 条全绿
即 checkpoint 全废、runner 报通过、测试一条不红。

修法:守结果,不守成因
scripts/checkpoints.js 结尾(if (failed > 0) 之前):
  if (results.length === 0) {
    console.error('no checkpoints were registered ...')
    process.exitCode = 1
  }
放 runner 而不是补回静态计数的理由:静态计数只能抓 helper 改名;
results.length === 0 抓的是结果,改名、被 stub、条件跳过都会红。

但 process.exit(0) 能绕过任何脚本内检查,所以另加一条外部断言
(test/phase-b-g-static.test.js)三层:
  - declared > 0                    helper 没被改名
  - 源码含 if (results.length === 0)
  - 该守卫确实设置 process.exitCode = 1
分工:runner 守结果,测试守 runner 还带着那道守卫。

实测
  0 条注册(stub)    -> no checkpoints were registered, exit=1
  条件跳过            -> 同上, exit=1
  正常态              -> 10/10 passed, exit=0
  变异:删 runner 守卫 -> not ok 6
  变异:helper 全改名  -> not ok 6

docs/dev-notes-mutation-criteria.md 增第 4 类
判据被替换时,旧判据的覆盖静默消失
- 形态:一条 test 守两件事
- 实测后果:0/0 passed / exit 0 / 120 全绿
- 守结果不守成因的四类成因对照表(含 process.exit(0) 是拦不住的那格)
- 自查问句:这条判据除了名字里那件事,还顺手守了什么
- 探针经验:不要用注释掉调用去验零注册,会变成 SyntaxError

验证
- npm test 120/120、npm run check 10/10
- npm test 3 次、npm run check 3 次(带 exit code 核对)全绿
- 无 .orig / .rej / /tmp/mut-* 残留
- 未 amend / 未 force push
2026-10-30 14:44:09 +08:00
c6595d2cf0 feat(memory-b): H 收尾 W1–W8 —— 信号转发、写侧校验、建中心入口
五条全部处理。第 5 条确认属实:saveSpaceCentroid 在 lib/ 里零调用点,
真实使用下中心表永久空着,票③ 的「dim unknown」会从边角变成常态。

W1/W5 信号转发(最硬的一条)
- execute(args, exec) → commit(input, {signal}) → embedText(text, signal)
- abort 时按「向量化失败」降级落盘:块写入、vector 留空
- 依据 dsh-tools/lib/types/index.d.ts:120-122

★ 一处我判错了,据实记录
W5 我原本断言「abort 时 isError === false」,实测是 true。查了实现:
dsh-tools/lib/index.js:3408 在工具成功后**再检查一次 signal**,
已 abort 就用 toolAbortedResult 覆盖成功结果。所以 isError 不归本插件控制,
断言它等于和 harness 对抗。改为断言**副作用**(块已写入、vector 为空),
并额外断言 error.info.code === 'ABORTED'。

W2 九个调用点补 await
scripts/checkpoints.js ×4、test/plugin.test.js ×5、test/loader.test.js ×2
(loader 两处保持并发语义用 Promise.all)。
原本绿是因为「无 embedder 时第一个 await 之前就写完 putBlock」——
这个隐式事实一旦被一行 await 打破,九处会静默变成「块没写」,
而且红会出现在离原因很远的地方(靠后面能否召回间接断言)。

W3 空指纹先判后嵌
原来顺序是 await embedText → putBlock 抛 → catch → 再 putBlock,
语义终点对但白付一次网络往返,且与 recall.js:430 的口径不一致。
改为 embed 之前判 space.fingerprint === ''。

W4 写侧维度校验
commit 现在检查 vector.length === space.dimension,不符按 embed failure 处理
(无向量落盘)。不落坏向量的理由:召回侧会按 skippedDim 静默跳过,
用户看到的是「记忆里有这条,但永远召不回来」。

W8 建中心入口(第 5 条)
新增 store.rebuildCentroid(fingerprint),移植 RebuildCentroid
(centroid_store.go:246-320):
  · 只取该空间的带向量行
  · 维度不符跳过不强制
  · 按 L2 归一化后累加均值
  · 均值范数 >= 0.6 才判 anomalous(centroid.go:87)
  · 计入 stats:meanLength / avgPairCosine / skippedRows
并新增 MEAN_THRESHOLD 常量。

★ W8 第一版判据是错的:我用完全共线的向量(meanLength = 1.0000)
  去测 0.6 阈值,把阈值改成 0.95 仍然通过。已改用 ±35° 锥
  (meanLength ≈ 0.879,落在 0.6 与 0.95 之间),并加断言
  「语料必须落在两阈值之间」,否则测试自身就失去区分力。

判据与变异(全部在 /tmp 干净副本上跑)
  W4  维度不符 → 块在、vector=''、fingerprint=''      | 删维度检查 → 红
  W5  abort → 块在、vector=''、elapsed<2s              | 不传 signal → 红(实测 5003ms)
  W6  无 embedder 时 {id,created} 与 putBlock 同形      | —
  W7  空指纹 → embedText 调用 0 次                      | 改为事后判 → 红
  W8  同空间重建、跨空间隔离、坏维度跳过、阈值判定      | 去空间过滤 → 3 条红
                                                        | 去维度过滤 → 1 条红
                                                        | 阈值 0.6→0.95 → 1 条红

未越界
- 四态计数(noVector/unknownSpace/spaceMismatch/embedFailure)留给 F/D
- recallBlocks 仍然同步、未接入 vectorScores(V 组的事)
- 票③「dim 来自中心元数据而非配置」继续有效;saveSpaceCentroid 仍在用
  dim ?? space.dimension,已在注释里标记为 C 组要区分「没建过」与
  「建过但维度取自配置」——声明不是证据

顺序按你的调整记下:H(完工) → V → C → F → D。
C 组准入条件已满足一半:机制(rebuildCentroid)有了,
「谁触发建中心」待 C 组定。

验证
- npm test 99/99(90 + W4/W5/W6/W7 六条 + W8 三条)
- npm run check 10/10
- 提交前连跑 3 次 test + 3 次 check,6 次全绿
2026-10-30 10:35:41 +08:00
3adb5a7018 docs+test: README 计数漂移根治 + MIG3 进 check + MIG4 崩溃窗口核实
README 计数漂移(你指出的)
- README.md 的 checkpoint 计数写的是 8/8,实际 9/9(现已 10/10)
- 证据块只列了 V1/V2/V3/V3b/V4/V5 六条,漏了 V3b† / V3c / A5†
- 改为**粘贴真实输出**,并加一段说明「本节曾两次因手抄而漂移」
- 新增自动校验(test/phase-b-g-static.test.js):比对 README 声称的
  checkpoint 数与 scripts/checkpoints.js 里 `await checkpoint('` 的个数,
  以及粘入的 "N/N checkpoints passed"。实测变异:把 10 改成 11 → not ok 6

MIG3 进 check(你的建议,采纳)
理由同意:check 是给人看的证据链,而「存量库能不能用」最该被看见;
将来谁改 open()/migrate(),看 check 的人比看 test 的人多。
新增 checkpoint MIG3:手工构造 phase-A 形库(含旧 centroid 表)→
opened → 两表列齐 → 旧块可读且 FTS 命中 → putBlock 成功 →
saveCentroid/loadCentroid 成功。现 10/10。

边角情况(你让我自己判断)——查了,结论与你的推测不同,据实记:
崩溃窗口(rename 成功但 user_version 未写入)**不可达**,
但原因不是我一开始写的那个。我最初注释说「因为 DDL 先跑会重建
graph_centroid」,写完后按纪律去验证,把 open() 里 migrate/DDL 顺序
对调,全量测试**仍然全绿** —— 说明那个解释是错的。

真正原因:pragma_table_info 对不存在的表返回零行,
所以 centroidColumns.length === 0,整个 rename 分支在重试时直接被跳过,
根本不会第二次 RENAME,与 DDL 顺序无关。

已按事实改正 lib/store.js 的注释,并把这个「我写错又验掉」的过程留在
注释里(否则下一个人会照抄错误解释)。新增 MIG4 钉住该恢复路径:
构造「旧表已改名、新表未建、版本未盖戳」的状态 → open 不抛错 →
重建可用 → 旧表行数不变。

验证
- npm test 84/84(83 + README 漂移守卫)
- npm run check 10/10(+MIG3)
- 提交前连跑 3 次 test + 3 次 check,6 次全绿
- 变异全部在 /tmp 干净副本上执行

D 组可以开工。
2026-10-30 10:26:38 +08:00
98612b2746 feat(memory-b): A 组完成 —— 异步 seam + golden fixture,recallBlocks 未动
G6 收口
- 范围如实收窄:表从 8 个扩到 **23 个**(当前 harness 里全部 user-role
  注入 kind),并把措辞改成「installed harness 里的 user-role 注入 kind」,
  不再暗示穷举。全量 MessageSourceMap 有 25 个 source key,会继续增长;
  白名单是 fail-closed,未列出的新 kind 天然安全。
- 每个 kind 都核到字面声明(不是照抄之前的表);`compact-checkpoint` 的
  kind 藏在 checkpoint.d.ts:18 的 marker 类型里,也已核到。
- 新增 **G6b**:`user-rpc` 的 kind 就是 `'user'`
  (dsh-api-session-controller/lib/types/types.d.ts:395-400),
  所以这条白名单不会把远程/网页用户挡在外面。这条是你指出的,
  我认为是全组最关键的安全性断言,已写进 README 正文。

A 组实现
- `recallBlocks` 保持同步且一行未改(新增函数在它之后)。
- 新增 `recallBlocksHybrid(store, embedder, query, opts)` async 包装。
  无 embedder / embedder 抛错 / 返回空向量 / 维度与 info() 不符
  ⇒ 退回同步召回,并在返回数组上挂不可枚举的 `degraded`
  ('no_embedder' | 'embed_error' | 'hybrid')。永不抛错(§0.4)。
- `degraded` 不可枚举:不进 JSON.stringify / Object.keys /
  与 phase A 输出的深比较,保证 A3 的逐字段相等成立。
- `Embedder` 窄接口三成员:embedText / info() 同步 / close。
  判据里不出现任何 provider 字段名。

A 组判据(test/phase-b-a.test.js,6 条)
- golden fixture:12 块 × 15 查询 × topK 1/3/5 = **45 例**,
  逐例比较 id / 顺序 / **score 精确相等(无容差)** / exact / why 字符串。
  容差比较会放过评分改动,而这正是本组要挡的。
- A1 结构性断言 recallBlocks 不返回 thenable。
- A3 hybrid(null) 与同步版逐字段相等(含 why)。
- A3b 降级原因可见。

A 组变异(全部实跑打红,原文如下)
  M1 非精确带分数 +1e-6
     not ok 2 - A2: golden fixture — every case reproduces exactly
  M2 recallBlocks 改 async(被否决的设计)
     not ok 1 - A1: recallBlocks is synchronous and takes no embedder
  M3 hybrid 给 why 加后缀
     not ok 3 - A3: recallBlocksHybrid with no embedder equals recallBlocks

★ A 组抓到的真问题(不是我的判据,是实现改动引起的)
`StableMem.recall()` 变成 async 后,上一轮那条 REGRESSION 变异测试
拿到的 Promise 没有 `.length` ⇒ `undefined < undefined` 为 false ⇒
断言「通过」但什么都没测。checkpoint V3c 同一处也有这个问题。
两处已补 await。README 里记成一条通用陷阱:
任何东西改 async 后,grep 所有对可能已变 Promise 的值取 `.length` 的地方。

验证
- npm test 40/40(34 + A 组 6)
- npm run check 9/9
- 提交前连跑 3 次 check + 3 次 test,6 次全绿(按你的要求)

F 组未动:等 §0.1 endpoint 规范化 / §0.2 四态 / §0.3 stats+is_anomalous /
§0.4 失败降级的裁定落地。
2026-10-30 10:05:32 +08:00
64c614b99a fix(memory): 自动召回在真实 dsh batch 里 100% 失效
实测发现的 bug(在 headless profile 里真跑出来的,不是判据抓到的)。

根因
lastUserText 取「最后一条 role=user」。但真实 step batch 里,
用户提问后面还跟着 user 角色的注入:
  [ 用户提问 ][ runtime-context 快照 ][ skill-catalog 提醒 ]
于是拿技能清单当检索词,召回 0 命中。

为什么 V3b 全绿却漏了:V3b 自造 batch 只有 2 条消息,判据有盲区。

修复
lastUserText 改为按 source.kind 过滤,allowlist 只有人真实提问的
kind(MessageSourceMap.user 的 'user')。未知 kind 一律当作注入——
将来某个插件的快照绝不能变成检索词。

  实际给 dsh 的注入 kind:
    runtime-context      dsh-agent-loop 的工作目录/沙箱快照
    skill-catalog        dsh-tool-skill 的技能提醒
    skill-invocation     同上
    agent-instructions / time-context / goal / ...

判据(先写红,再修)
- REGRESSION: lastUserText 从 3 段 batch 里取出真实提问        修前红
- REGRESSION: 真实 batch 端到端必须召回命中(修前 0 命中)      修前红
- REGRESSION 变异: 退回「最后一条 role=user」⇒ 命中数必须掉到 0  修前红
- checkpoint V3c: 同样的场景进 checkpoints,V3b 的盲区补齐
- 原 V3b 的 fixture 补上 source.kind(真实消息都有,缺了就没测到判别)

修前/修后数字(4 条种子块,三种 batch 形状)
  batch 形状                                旧规则  新规则
  2 段(原判据形状)                            1       1
  3 段(+runtime-context)                      0       1
  4 段(+runtime-context +skill-catalog)        0       1
  合计                                          1/3     3/3

在实测者留下的真实库上复验(/tmp/dsh-memory-probe/memory.db,
1 条真实块「测试口令是 7788。」):
  旧规则 query = "[Skills] git-release-discipline, ..." → 0 命中
  新规则 query = "测试口令是什么"                        → 1 命中

验证:npm test 29/29(原 26 + 3 条回归)、npm run check 9/9(原 8 + V3c)。

README 补「真实 batch 不只是提问」一节,含 kind 对照表与修前修后数字,
并把 V3b 的盲区显式标注为已知缺口。
2026-10-30 09:55:14 +08:00
114fae84aa feat(memory): MVP 阶段 A 切片 —— 块进 dsh、召回、进 prompt
按 docs/feasibility.md §7.1 阶段 A 落地最小可验证切片,
刻意不引入 embedding、不碰 L1 淘汰。

落地内容
- lib/store.js   BlockStore:node:sqlite 建 memory_blocks /
                 memory_block_edges / graph_centroid 三表 + FTS5 索引。
                 内容派生 id(blk_+sha256[:12],移植 distill.BlockID);
                 putBlock 走「空值不覆盖」,重复 commit 幂等。
- lib/recall.js  FTS5 bm25 + 精确串布尔置顶(移植 SymbolScore/fusion.go:427)、
                 单字符不成符号(abstain.go:115)、前缀特异性 tie-break、
                 CJK 2..4 字滑窗兜底(移植 cjkWindows/fusion.go:240)。
- lib/index.js   StableMem service + systemPrompt.context 常驻索引 +
                 agent/pre-step 按本轮输入召回注入 + stablemem_commit 工具。
- scripts/checkpoints.js  V1–V5 + 两条变异,对真实 cordis 服务跑。
- test/          26 个测试(store / recall / plugin wiring / real loader / isolate)。
- scripts/link-dsh.sh     把全局 dsh 包软链进 node_modules(插件本身零依赖)。

三个实测出来的 dsh 约束(都是先红了才发现的)
1. cordis service 不能用 JS #private 字段 —— ctx.<name> 是 proxy,
   访问私有字段抛 "Cannot read private member"。状态必须放普通属性。
2. isolate() 不隔离 prompt context 名与工具名(只隔离 service 查找)——
   所以工具名做成可配置,prompt context 名由 db 路径派生以保唯一。
3. systemPrompt.getContextOrder() 是分配器不是常量表,用命名槽位。

验证
- npm test  → 26/26 通过
- npm run check → 8/8 checkpoint 通过
  V1 sqlite/WAL/100 写回读;V2 能力分层(tier1 30/30、tier2 20/20,
  tier3/4 作为已知缺口显式记录);V3 AssembleContext 仅 [agent];
  V3b 注入落进批次且带 source.kind;V3b† 静默召回则判据红;
  V4 工具 schema+render 端到端;V5 fiber dispose 释放句柄并可重开;
  A5† 退化成包含匹配则区分度消失。

阶段 A exit criteria 全部满足(含 V3/V3b 分工结论写入 README)。
参考仓 /home/program/TrueAgent 未改动。
2026-10-30 09:44:42 +08:00