Commit Graph

5 Commits

Author SHA1 Message Date
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
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
885e0b66a3 test(memory): 补 G6/G7 两条判据,把真实 batch 教训写进 README
按要求补两条,均在补之前先跑红。recall.js 本轮不动(零改动已核)。

G6 —— 白名单判据用真实 kind,不测自造名字
上一版只测了自己猜的 `runtime-context`。现在参数化覆盖 8 个真实
注入 kind,每一条都是从已安装包里查出来的字面量:

  runtime-context      dsh-agent-loop/lib/types/runtime-context.d.ts:12
  skill-catalog        dsh-tool-skill/lib/index.js:984, :1008
  skill-invocation     dsh-tool-skill/lib/index.js:913
  schedule             dsh-schedule/lib/index.js:1594
  agent-message        dsh-subagent/lib/index.js:446
  user-question-reply  dsh-user-questions/lib/index.js:563
  time-context         dsh-time-context/lib/index.js:237
  agent-instructions   dsh-agent-instructions/lib/index.js:904

反向变异:把 HUMAN_PROMPT_KINDS 放开成「role 是 user 就收」⇒
必须对全部 8 个 kind 都失败(不只是其中几个)。

G7 —— kind === undefined 必须返回 ''
实现本来就是 fail-closed,但按要求用断言钉死,不依赖推断。
覆盖 5 种形态:source 缺失 / source 为 null / source.kind 缺失 /
kind 非字符串 / kind 为空串。
变异:去掉 typeof kind !== 'string' 守卫 ⇒ G7 红。

★ 关于 G7 的可达性,查证后如实记录:当前 harness 下这个状态
不可达。assertMessageEventShape 要求 source.kind 为非空字符串,
且 append 路径(dsh-session/lib/index.js:1089)与 seed/restore 路径
(:1165)都跑它。所以 G7 是纵深防御(防未来 format 变更),
不是活 bug。已在 README 和测试注释里写明,不夸大成"修了个洞"。

实测变异验证:删掉 kind 守卫后 G6/G7 四条全部变红,恢复后全绿。

README 补:
- kind 对照表改为带真实来源行号的 8 行
- 新增「★ 教训:2 条消息的 batch 是假 batch」小节:V3b 之所以让
  这个 bug 漏过,正是因为它选了自己构造的 batch 形状;规则是
  凡测 agent/pre-step 的判据一律走 realBatch()
- V3b 保留(仍钉注入归因)但标 V3b⚠ 已知盲区
- 测试表列出 G6/G7 及其变异

顺带:realBatch() 从用例之间提到文件顶部共享位置,V3b 也改用它
(原先是手造单条消息)——否则"必须走 realBatch()"只是一句空话。

验证:npm test 33/33(原 29 + G6/G7 四条)、npm run check 9/9。
2026-10-30 09:59:17 +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