|
|
5b217c1f78
|
feat(memory-b): expose V3 vector and symbol attribution
|
2026-11-01 15:34:55 +08:00 |
|
|
|
b7eff5db66
|
test(memory-b): strengthen C3 recall and corruption mutants
|
2026-11-01 12:39:53 +08:00 |
|
|
|
d73d47c081
|
feat(memory-b): port C3 centroid staleness and async rebuild scheduling
|
2026-11-01 12:32:16 +08:00 |
|
|
|
d57c9dc798
|
feat(memory-b): port V2 non-exact fusion weights
Also clear three stale counts the release train authorised: criteria A1
wording (sync + hybrid), criteria G1 counts (173/12), README test-site count.
|
2026-11-01 12:01:22 +08:00 |
|
|
|
fc92db9f5b
|
feat(memory-b): port V1 exact vector pin adjustment
|
2026-11-01 11:51:13 +08:00 |
|
|
|
8bdd9f5ce0
|
test(memory-b): make F8 mutants bite the positive criteria
|
2026-11-01 11:11:37 +08:00 |
|
|
|
96adb172eb
|
test(memory-b): add F8 checkpoint for width validation
|
2026-11-01 11:01:44 +08:00 |
|
|
|
b5d3f6ec73
|
feat(memory-b): add F7 fingerprint space stats
|
2026-11-01 10:50:46 +08:00 |
|
|
|
f62b826c64
|
test(memory-b): 补 unknownSpace / dimensionMismatch 的文案判据(四态齐)
你的复核成立:renderUnavailableTail 每态一个 clauses.push,但只有
spaceMismatch(D5b)与 noVector(D5c)有判据。删掉另两段,全套仍绿 ——
它们在渲染层是装饰,与恒 0 的 skipped_dim 同型。
我复现时先得到「各红 1 条」,查下去发现那是副本缺 README 导致
「README states no counts」那条报 ENOENT 的噪音,不是判据咬住。
去掉噪音后确认:你的结论正确,两态确实全绿。
补四条(每态一条判据 + 一条抑制变异)
D5e unknownSpace:断言「1 without a space label」,且断言文本里
**不含** browse / exact text —— 该态走语义路,借用 noVector 的
「可浏览」说法会误导(这是 F6 那条 browse≠recall 口径的第三次复用)
D5f dimensionMismatch:断言「1 whose width does not match this provider」
**且**指向「fix the config」这一补救,且不含 browse
D5e† 删 unknownSpace 的 clauses.push ⇒ 必须红
D5f† 删 dimensionMismatch 的 clauses.push ⇒ 必须红
真变异实测(删整段 clauses.push)
删 unknownSpace → not ok D5e、not ok D5e† (2 红)
删 dimensionMismatch → not ok D5f、not ok D5f†(2 红)
每条变异同时咬住「本体判据」与「自己的 suppression 测试」,不是只红一条。
两处写作教训
- D5f 的构造必须走 raw putBlock:F8 正确地拒绝显式传错宽度,
所以「库里有窄向量行」现实中只来自历史数据或修复脚本
- D5e†/D5f† 初版只 new Context() 没装 systemPrompt/tools 服务,
dispose 时 "Cannot read properties of undefined (reading 'context')"
—— apply() 的 inject 需要它们,已补
现状:四态 noVector / unknownSpace / spaceMismatch / dimensionMismatch
在**渲染层**各有判据与抑制变异,不再有装饰态。
验证
- npm test 166/166(162 + D5e/D5f/D5e†/D5f†)
- npm run check 10/10
- 干净副本实数见后续消息
|
2026-10-30 17:26:16 +08:00 |
|
|
|
99d0670ebe
|
fix(memory-b): 返工 ③④ —— trigger 直判据 + skipped_dim 不再是假字段
③ trigger 补直写判据(此前只有间接覆盖)
你的复核准确:摘掉 trigger 的 WHEN 会让 F3/D3 红,所以「间接有守卫」——
但没有一条盯 raw-writer。而 trigger 存在的全部理由就是防 raw-writer
(修复脚本、迁移、运维手敲 sqlite3)。putBlock 自己就能把列写对,
所以真实风险是「trigger 全没了也没人发现」。
MIG5 三条:
- raw UPDATE 把 vector 改成长度不符 ⇒ vector_dim 被拉回真值;清空则归 0
- raw INSERT 带错误 vector_dim ⇒ 被 trigger 覆盖为真值
- MIG5† 分别 DROP 两个 trigger,各自断言「谎言能存活」
真变异:从 DDL 里删掉整段 trigger ⇒ MIG5 前两条红(MIG5† 仍绿,因为它
主动 DROP,不依赖 DDL 是否存在 —— 这是两者分工正确的标志)。
④ skipped_dim 恒 0 是假字段,选 (a) 补维度过滤
你的诊断对,且比我的注释更狠:一个永远不能非零的字段,配合
`typeof === 'number'` 的断言,把 F4「维度失配与指纹失配分开计数」
做成了摆设。这与更早那处 assert.equal(skipped, 2) 是同一类。
实现:
- searchFts / 包含兜底都携带 dimension(来自 vector_dim 列)
- recallBlocks 新增 dimension 选项,在**空间过滤之后**做维度过滤
★ 顺序有意:先空间后维度,保证一块同时 stale 且窄时只计一次(记 space),
否则两个计数会重复计同一行、谁也加不出「被丢弃的块数」
- skippedDim 与 skipped 分别挂在返回数组上(F4 要分开)
- recallBlocksHybrid 把 space.dimension 注入 scoped,六个降级分支自动继承
- lastRecall.skipped_dim 改为读真实值
判据 F4b 四条:
- 同指纹不同宽度 ⇒ skippedDim=1 且 skipped=0(不串桶)
- 同时存在旧空间块与窄向量块 ⇒ skipped=1、skippedDim=1,不双计
- last_recall.skipped_dim 能非零
- F4b† 变异:删维度过滤 ⇒ skippedDim 归 0、窄向量被召回 ⇒ 红
实测:hits=[C 正常],skipped=1,skippedDim=1,两者都能非零。
一处测试构造的调整:窄向量块用 raw 写入而非 commit —— F8(正确地)
拒绝显式传错的宽度,所以「库里存在窄向量行」在现实中只能来自
历史数据或修复脚本,与 trigger 那条判据的假设一致。
文档
- criteria: F4 补「两个都能非零」与恒 0 字段的教训
- dev-notes 增第 6 类「恒为 0 / 恒为真的字段」,含自查问句
「这个字段的取值集合里,有没有一个能让我这条断言失败?」
验证
- npm test 162/162(155 + MIG5 三条 + F4b 四条)
- npm run check 10/10
- 干净副本实数见后续消息
|
2026-10-30 17:07:19 +08:00 |
|
|
|
d9280773aa
|
fix(memory-b): 返工 —— last_recall 可查询 + renderIndex 按态分述
① embedFailure 有落点了(此前只有注释)
你的判断准确:degraded 是**单次调用的返回值**,调用方看得见但运营方查不到;
criteria:282 要的是「可查询」,两者不能互相顶替 —— 就像 states 不能顶替 skipped。
实现:
- StableMem.lastRecall:最近一次 recall 的 {mode, skipped_fp, skipped_dim,
embed_error, at},由 recall 路径写入
- stats() 增加 last_recall 字段(形状照 criteria:282)
- 健康召回记 'vector_ready_unused'(A 组语义),失败记 'embed_error'/'aborted'
- **不进四态**:那是从行派生的库状态,塞进单次事件会让同一时刻读两次
得到不同数字 —— 正是 D4 要避免的
判据 3 条:真 adapter 抛错后 stats().last_recall.mode 读得到该次降级;
干净召回会覆盖;D3b† 变异(删掉写入)⇒ 判据红。
② renderIndex 按态分述,不再一句包四态
你实测的四态揉成一句是真的错:旧句尾「still searchable by exact text」
对 noVector 成立、对 spaceMismatch 在生产召回路径上**不成立**
(带 fp 召回 0 条,不带 fp 浏览 1 条 —— 与 F6 裁定的 browse≠recall 同一条口径)。
改成分态子句:
spaceMismatch → 「from a retired space (reachable by browse only, not by recall)」
unknownSpace → 「without a space label (semantic recall unavailable)」
dimensionMismatch → 「width does not match this provider (fix the config, then re-embed)」
noVector → 「not yet embedded (still reachable by exact text)」
③ 无 embedder 走常态静默(P2,一并改)
无 embedder 是出货默认,不是异常。原来 2 块 noVector 每轮都被告知一次
「召回不了」,而与 exact recall 仍能命中相冲突,且与 W11e 的静默口径相反。
现在 space() === null 直接返回 ''(只留计数行);运营侧从 stablemem_stats 查。
测试用 reportNoVectorAlways 钩子单独覆盖该措辞,生产默认 false。
判据 4 条:D5b(mismatch 必须说 browse、不得说 still searchable)、
D5c(noVector 说 text recall,且先实证 exact recall 真能命中)、
D5d(无 embedder 时不重复告知)、D5b†(单句尾部 ⇒ 红)。
另把 D5/D5† 改为需要配置空间——它们原来在无 embedder 下断言,
与 D5d 的新口径直接冲突。
实测两场景
A 无 embedder:renderIndex 只报「2 durable note(s)」,无警告;states noVector=2
B 旧空间块:renderIndex 报「1 from a retired space (reachable by browse only,
not by recall)」;states spaceMismatch=1;生产召回 0 条 skipped=1
验证
- npm test 155/155(151 + 返工 4 条)
- npm run check 10/10
- 干净副本实数见后续消息
|
2026-10-30 16:49:55 +08:00 |
|
|
|
160b5b5139
|
fix(memory-b): 三项验收整改 —— 去写死数字 / F8 硬报错 / blockStates 去 JSON.parse
一、skipped 断言不再写死数字
原 assert.equal(hits.skipped, 2) 属于「手抄数字」——与刚删掉的 README 计数同类:
fixture 一改就悄悄不再描述事实。
改为从构造库现算:expectedSkipped = counts.unknownSpace + counts.spaceMismatch,
并先断言 expectedSkipped > 0(防止变成「0 == 0」的假通过)。
二、F8 从「事后统计」改成「写侧硬报错」
原状:写侧维度校验只降级成「不嵌入」,坏向量仍能落库,靠 blockStates
的 dimensionMismatch 事后兜住 —— 那是 accounting,不是 validation,
与 criteria:165 的「硬报错」不符。
实现:putBlock 新增可选 expectedDimension,声明了宽度且不符 ⇒ 抛错不落库。
commit 在「调用方显式传 vector」时传 expectedDimension。
★ 两个入口必须分清,否则会与 W4 打架:
- 调用方 bug(显式传了宽度不对的向量)⇒ **必须抛**,否则它以
「块在、召回不了」的形式沉默很久
- provider 异常(embedding 返回宽度不对)⇒ 按 W4 降级为不嵌入,块仍写入
(把上游抖动抛给用户 = 让一次 provider 故障变成写入失败)
判据含三例:显式传错宽度抛;显式传对宽度通过;provider 返回错宽度降级不抛。
F8† 变异:把宽度检查改成恒不触发 ⇒ 坏向量落库 ⇒ 判据红。
三、blockStates 不再解析向量(性能账)
实测 2000 块 × 2048 维:
改前 354ms/次(其中 255ms 是纯 JSON.parse)
改后 4ms/次(约 90×)
而 renderIndex 每轮都调它 —— 这是挂在 pre-step 热路径上的固定成本。
做法:memory_blocks 新增 vector_dim 列(维度是行事实,不是负载内容),
- putBlock 写入时一并写
- 迁移时为存量行回填一次(只此一次,不在读路径)
- 加两个触发器保证 vector 与 vector_dim 永不脱节:
★ 理由:既然这一列的存在就是为了「不解析负载」,那每个写入者都背上了
保持一致的义务。漏一个写入者(修复脚本、测试 fixture、未来迁移)
就产生一行「维度是谎话」的数据 —— accounting 报它 active,
而召回静默跳过它。用触发器让这个状态**不可表示**,而不是只劝阻。
- 静态判据:blockStates 函数体内不得出现 JSON.parse(防以后「顺手简化」改回去)
四、embedFailure 记账
不进四态(它是单次调用事件,不是库状态),criteria 已补说明:
四态是库状态、从行派生;embedFailure 属于 recallBlocksHybrid 的 degraded,
由 W 组覆盖。塞进库状态会让「同一时刻读两次得到不同数字」,正是 D4 要避免的。
顺带修:F4† 的变异锚点因 blockStates 改写而失效(withMutatedCopy 显式报
「anchor not found」——这条守卫按设计响亮失败,没有静默变绿)。
验证
- npm test 148/148(143 + F8 四条 + 性能守卫一条)
- npm run check 10/10
- 性能实测见上
- 干净副本实数见后续消息
|
2026-10-30 16:29:05 +08:00 |
|
|
|
b73607ed02
|
feat(memory-b): F3/F4 四态派生计数 + D3 工具面 + D5 提示渲染
顺序遵守裁定:F2 过滤先落地(上一条 425c441),这一条才加计数。
先做计数会数出一个恒为 0 的假指标。
四态定义与归属
noVector vector 为空 回填
unknownSpace 有 vector 但 fp 为空 重嵌(说不出来属空间)
spaceMismatch 有 vector、有 fp ≠ active 重嵌到当前空间
dimensionMismatch 有 vector 但宽度不对 改 provider 配置(F4:与上面分开)
embedFailure 本次调用拿不到向量 ← 不在这里,它是单次事件不是库状态
F3/F4 判据(4 条行为 + 3 条变异)
F3 四态从库派生;skipped 非零(实测 skipped=2,不是恒 0)
F4 dim 与 fp 分开计数,且不互相重复计入
F3 改库/删库后计数跟着变(证明不是内存累加,D4)
F3 skipped 在符号路也计(fusion.go:495 同款:只测向量路等于没测)
F3† 内存累加器实现 ⇒ 「跟随数据库」断言红
F3†symbol 计数只挂在向量路 ⇒ 符号路的 skipped 断言红
F4† 把 dim 并进 spaceMismatch ⇒ 分桶断言红
D4:从 DB 派生,不累加
store.blockStates(activeFp, activeDim) 每次扫行计算,无缓存无累加。
判据用「绕过插件直接 UPDATE / DELETE,计数必须跟着变」证明。
D3/D5:两个消费者,一个来源
- renderIndex() 与 stablemem_stats 都调 blockStates(),不各留一份
- D5 的提示读同一个 unavailable 计数(noVector+unknownSpace+
spaceMismatch+dimensionMismatch),说明「N 条暂不可语义召回,
仍可按精确文本检索」
- D3 判据含无空间/有空间两例:无 active space 时 mismatch 必然为 0
(没有参照物),这条我第一版判据写错了,已按实际语义修正
★ 一处新工具引入的回归(已修)
新增 stablemem_stats 时把工具名硬编码,第二个实例挂载时撞名
"tool 'stablemem_stats' is already registered" —— 与之前 prompt context
名同型:isolate() 不分区工具名。已改为可从 config.statsToolName 覆写,
loader 测试同步传参。
★ 一处「注册了但跑不到」的坑(已修)
registerTools 里我用了 return ctx.tools.register(...),后续第二个
register 永远不可达,但文件读起来像注册了两个。已去掉 return 并注释。
验证
- npm test 143/143(138 + F3/F4 四条 + 变异三条 + D3/D5 两条)
- npm run check 10/10
- 实测数字(5 块构造库):
blockStates('fp-cur',3) = {total:5, active:1, noVector:1,
unknownSpace:1, spaceMismatch:1, dimensionMismatch:1}
recall("缓存", fp-cur) → 返回 1 条, skipped=2
recall("4324", fp-cur) → 返回 2 条, skipped=2
- 干净副本实数见后续消息
|
2026-10-30 15:59:22 +08:00 |
|
|
|
48c52497c2
|
feat(memory-b): F6 三段口径固化成判据 + A1b 降级记账
裁定:F6 选 (a),措辞改写,不留互斥读法
原措辞「仍可按文本调出、不参与向量召回」读起来像互斥。实际是三层:
「按文本调出」= store 级无空间浏览,**不是生产召回路径**。
不改 F2 去迁就 F6 字面自洽的理由:F2 是 fusion.go:495 同款安全缺陷修复,
优先级高于 F6 的措辞。所以 F6 改成三段并列,每段一条断言,
**同一个用例内全断言**:
数据层:指纹变更后旧块仍在库中 —— 块数不减、按 fp 分组两行都在
浏览层:store 级不带空间参数的文本查询仍能取到旧块
召回层:带 fingerprint 的生产召回路径必须把旧块过滤掉(F2 承担)
为什么必须一个用例:只断言「过滤掉了」会漏掉数据被误删
(删库在召回层看起来就是「过滤正确」);只断言「能调出」会把 F2 挡回去。
F6 判据 3 条
F6 三段全断言 + 反向对照(当前空间块两层都能取到,
防止断言因为「什么都找不到」而假成立)
F6†data 变异:putBlock 顺手 purge 其它空间的行走道 ⇒ 数据层断言红
F6†recall 变异:去掉空间过滤器 ⇒ 召回层断言红
两条变异各自只咬自己那一段,与 F6†data/†recall 的分工一致。
★ F6†data 的变异写法踩了三次坑(据实记)
1. 只写 form(没真改代码)—— 那不是变异,是自测
2. 注入 SQL 里的转义引号把测试文件写成 SyntaxError
3. 最终用 String.fromCharCode(39) 拼引号 + 数组 join 构造注入文本,
避开嵌套模板字符串的转义地狱
裁定 2:A1b 降级要记账
A1b 原先逐字节锁死 recallBlocks 源码,F2 合法地要改它,已降级为
「不传 fingerprint 时行为不变」。但降级后的 A1b-behaviour 只跑 golden
**前 5 个** case,变异 A(非精确带分数微调)下它可能是绿的。
已在判据注释写明:**结论不由 A1b 单独给出,由 A2 兜**
(A2 走全部 45 例、比精确分数);A1b-behaviour 只是烟测,不是权威。
同步进 memo:docs/dev-notes-mutation-criteria.md 增第 5 条
「判据降级时,覆盖面必须一起交代」—— 这不是失效模式,是记账纪律。
自查问句:这条判据降级后,原本由它兜住的结论现在由谁兜?
答案必须是一个具体的、覆盖更广的判据名。
验证
- npm test 131/131、npm run check 10/10
- 干净副本(git archive + link-dsh)实数见后续消息
|
2026-10-30 15:39:03 +08:00 |
|
|
|
425c441470
|
feat(memory-b): F5 固化为 fail-closed 判据 + F2 符号路空间过滤
裁定 1(F5):fail-closed,写侧直接拒收
docs/phase-b-criteria.md §0.2 从「待拍板」改为「已定」:
无名向量的块直接拒收,比放过再靠下游兜更能守住空间隔离;
安全语义优先,**不为 HA 的疑似缺陷同构**
(HA 的 block_recall.go:140 三个 && 让空 fp 被放行,这是要修正而非复刻的行为)。
落点分两层并写明「缺一层都不算落实」:
写侧 putBlock 抛错(已实现)
读侧存量行归 unknownSpace、不参与向量召回(F 组)
F5 判据(5 种输入形态 + 变异)
省略 / 空串 / undefined / 数字 / null 一律抛错,且都不落库
合法形态仍工作(无向量无 fp;有向量有 fp)
F5† 变异:删掉写侧守卫 ⇒ 无名向量被接受 ⇒ F5 必红
顺带修一处:lib/store.js 的 vectorFP 加 String() 归一。
原先变异摘掉守卫后会以 SQLite bind 错误崩,掩盖了真正的断言失败
(变异因为错误的原因变红 = 不算证据)。
裁定 2(F2):先过滤,后计数
F2 —— 空间过滤必须覆盖符号路,不只向量路
- store.searchFts 现在返回 fingerprint / hasVector,
两条 intake 路径(FTS 与包含兜底)都携带身份字段
★ 兜底路径第一版漏了这两个字段,导致过滤器看到 hasVector === undefined
而放行 —— 正是「过滤器只覆盖两个入口中的一个」那种 bug
- recallBlocks 新增 fingerprint 选项,在 pool 构建后统一过滤
刻意**不复刻** HA 的三连 &&:本仓 fail-closed,
带向量但无法命名空间的块一律排除(§0.2 裁定)
无向量的块不受影响(它们本来也不参与向量路,文本仍可检索)
- recallBlocksHybrid 在函数入口解析一次空间并注入 options,
六个降级分支全部经 scoped 调用 —— 逐调用点设置会在加第七个分支时漏掉一条
F2 判据(4 条行为 + 2 条独立变异)
F2 符号路命中旧空间块 → 被排除
F2 不传 fingerprint → 过滤器不生效(phase A 行为不变)
F2b 有向量但 fp 为空 → fail-closed 排除(HA 此处会放行)
F2 hybrid 在所有降级路径上应用同一过滤
F2† 删掉过滤器 → 旧空间块回来
F2†b 让 searchFts 不携带身份 → 过滤器失明(过滤只剩一个入口)
A1b 的调整(必须说明)
A1b 原先逐字节锁死 recallBlocks 源码,而 F2 合法地需要改这个函数。
改为守它真正要守的东西:**不传 fingerprint 时行为不变**
- 静态:过滤器必须 gated 在显式传入的 fingerprint 上
- 行为:golden fixture 前 5 例逐字段一致
依据:golden 语料 12 个块全部没有 fingerprint,
所以 F2 对 phase A 是纯增量;A2 仍全绿即为证。
验证(干净副本:git archive → 空目录 → link-dsh → 自跑)
见下一条消息附实数。
未 amend / 未 force push。
|
2026-10-30 15:29:23 +08:00 |
|
|
|
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 |
|
|
|
d1442848c4
|
docs: README 补前置准备(干净 clone 直接跑 npm test 会红 4 个文件)
缺口(评审复现,我独立复现确认)
git archive HEAD 到空目录、原样 npm test:
# tests 34 # pass 30 # fail 4
not ok test/loader.test.js / phase-b-g.test.js / phase-b-h.test.js / plugin.test.js
根因:Cannot find package '@deepseek-ai/cordis'。
而 README 全文对 link-dsh.sh / node_modules / 前置准备零命中 ——
正文只写「npm test 是 how many 的权威」,默认读者已经跑过 link-dsh.sh,
而这个前提谁都没写。
这与本轮刚修的那个缺口同族:判据/文档声称的能力,在没有守卫的地方悄悄不成立。
(刚删掉 README 计数,理由是「没守卫就会漂」;「npm test 是权威」这句话
同样没有守卫 —— 它默认了一个没人写下来的前置。)
修法(按裁定选 ①,不用 pretest 钩子)
README §Verify 开头新增「Prerequisite: link the harness packages first」:
- 说清本仓零运行时依赖,@deepseek-ai/* 全部是 peer,
必须来自机器上已装的 dsh(vendoring 会与它要运行的 harness 漂开)
- 给命令 bash scripts/link-dsh.sh
- 明确写出跳过它的症状:四个文件 ERR_MODULE_NOT_FOUND
(loader / plugin / guard / http 四套,与实测逐一对齐),
并说明这是缺前置的正常形态,不是树坏了
- 补非默认安装根的可选参数
按裁定的第二条不加判据去测「干净 clone 能跑测试」(成本高于收益),写清楚就够。
顺带一处:README 的计数禁令只匹配数字形态(119 tests / 10 checkpoints /
10/10 passed),新段落用了英文「four files」,不被误报 ——
在守卫里加注释说明这个 scope,以及为什么不去匹配文字形态
(「四个文件报 ERR_MODULE_NOT_FOUND」是合法的散文,再往下写就变成小解析器)。
验证
- 干净 clone 复现:无链接 4 红;bash scripts/link-dsh.sh 之后 120/120、check 10/10
- README 描述的失败文件清单与实测逐一一致
- npm test 连跑 5 次 120/120
- npm run check 3 次,exit=0、10 条全绿
- README 计数禁令仍绿(未误报新段落)
- 无 .orig / .rej 残留,未 amend / 未 force push
|
2026-10-30 15:02:38 +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 |
|
|
|
fc5fe11590
|
docs: 去掉 README 计数并在判据侧禁止它 + 变异判据方法论 dev-notes
1. README 计数去掉(不留守卫)
去掉「119 tests」与「10 checkpoints / 10/10 checkpoints passed」。
理由按裁定:一个没有守卫、被手改过五次的数字是负资产,
它给人的确定感是假的;加守卫要翻倍套件时间,不值。
守卫反过来断言「README 不得出现计数」:
/\b\d+\s+tests\b/i
/\b\d+\s+checkpoints\b/i
/\b\d+\s*\/\s*\d+\s+checkpoints passed/i
三种写法实测都能抓住(120 tests / 12 checkpoints / 12/12 passed)。
顺带处理一处同类:原文「(6 tests)」是手抄的测试数,删;
「12 blocks × 15 queries = 45 cases」是 fixture 自身内容量,也删并改为
指向 fixture 文件 —— 由文件决定的东西不该在文档里再写一份。
(README 现在的口径:表格说「验什么」,`npm test` / `npm run check` 说「多少」。)
2. docs/dev-notes-mutation-criteria.md(新)
把三轮踩出来的方法论落成仓库文档,README 的 Verify 段加指针。
三类失效模式,每条都给「形态 / 实测证据 / 正确形态 / 自查问句」:
① 自测型假变异(犯四次:W11† / W11d†a / W11d†b / W12†)
变异在 test/ 里自己拼替身,一行 lib/ 没碰 ⇒ 真回退下保持绿
实测:M-a / M-b 下 W11d†a / W11d†b 均绿,只有 W11d 红
② 清单写死(W11f)
手写 MODES 漏掉新 case;future_mode_a/b 测的是「掉进 default 的」,
永远测不到「有人新写了一个 case」
实测:植入 brand_new_mode(零命中时撒谎)→ 写死版全绿,扫描版红
并记下边界:自动推导不能替代「提取本身也要被守卫」,
所以还需要 declared.length >= 4 + W11f-a(guards the guard)
③ 变异共享状态并发撞车(我自己引入的)
就地改写 lib/*.js,而 node --test 并行跑文件 ⇒ 别的文件读到中间态
实测:约 1/30 偶发失败,症状像 flaky 断言;A1b / G2b / G5 这类
读源码的判据会读到被改坏的中间态
修法:在 lib/ 副本上变异(复制 lib + package.json + 软链 node_modules),
finally 清理,锚点失配显式失败
文末给通用自查清单 9 条 + 一句话记住三条。
验证
- npm test 119/119、npm run check 10/10
- 连跑 6 次全量 0 失败
- 无 .orig / .rej / /tmp/mut-* 残留
- 未 amend / 未 force push,199553f 之后是新提交
|
2026-10-30 14:36:41 +08:00 |
|
|
|
199553f91a
|
test(memory-b): W11f 模式清单改为从源码 case 字面量推导
接你备好的两个加固 patch(w11f-hardening-test / -README),
应用后 md5 与期望一致,lib/ 零改动:
test/phase-b-h.test.js 655fd63d8c18d88a7b7885868654fb80
README.md dcf50c6138a8357ca21363f09a104597
lib/recall.js c8fdffd25d5340291adf6cb26e1741f(未变)
盲区(我在 d972a2d 上独立复现了)
在 lib/recall.js 里植入一个会撒谎的新分支:
case 'brand_new_mode':
return '[Memory] New mode degraded; matches below may be incomplete.'
旧版 W11f / W11f† / W11f†b 三条**全绿**。
根因:MODES 是手写清单,新 case 不在里面。
而 future_mode_a / future_mode_b 那两个名字有误导性 ——
它们测的是「掉进 default 的未知模式」,永远测不到「有人新写了一个 case」。
而正常演进恰恰就是新写 case,那时 default: 与手写清单**两头都管不到**。
加固(补丁采用)
- 模式清单从源码 case '<literal>' 扫出来,不再手写
- declared.length >= 4 + 三个必需 mode 的存在性断言:
提取失效就响亮失败,而不是迭代空列表照样绿
- 新增 W11f-a(guards the guard):用变异往源码塞一个新 case,
断言扫描能看见 —— 因为「扫描器坏掉」是这套判据唯一能静默失效的方式
- 追加 'mode_with_no_branch_at_all' 覆盖「无 case、落 default」那一态
同一植入下新旧对比(我实测)
旧版(写死 MODES) → # pass 3 / # fail 0,漏
新版(源码扫描) → not ok W11f
'brand_new_mode + zero hits must not promise entries below;
got: [Memory] New mode degraded; matches below may be incomplete.'
另做了两个更狠的探针,确认它不会静默失效:
- 函数改名导致导入失败 → 整个文件响亮失败(不是静默绿)
- 保留函数名但把 case '<literal>' 全改成常量引用(语法合法仍可导入)
→ not ok W11f: 'expected at least 4 case literals in degradationFound,
found 0: ' —— 正是设计意图
也没变松:W11f†(删 aborted 分叉)/ W11f†b(删 default 分叉)仍各自只咬自己那半。
验证
- npm test 119/119(+W11f-a)
- npm run check 10/10
- 连跑 8 次全量 0 失败
- 变异 runner 零残留(/tmp/mut-?????? = 0)
- 未 amend / 未 force push,d972a2d 之后是新提交
|
2026-10-30 14:13:39 +08:00 |
|
|
|
d972a2d135
|
fix(memory-b): W11f —— aborted / default 零命中时不再承诺条目
接你备好的四个 patch(ready-lib / ready-test / README / docs),
应用前逐个审过内容,dry-run 干净,应用后 md5 与你给的期望值一致:
lib/recall.js c8fdffd25d5340291fadf6cb26e1741f
test/phase-b-h.test.js c06685abb7672fa806ca37e0d1cb8947
README.md 3c183b045dd7de4a54338ed9d01b3738
问题(我上一轮修 embed_error 时只修了那一个分支)
aborted 与 default: 在零命中时仍说「matches below are …」,而下面没有条目。
aborted + 零命中不是边角 —— 用户点停止、且这条查询本来就查不到东西,
是取消最常见的形态。
default: 更糟:它是未来任何未知模式**唯一**会走的路,
最不该在这里开始编。
修法(补丁采用)
把「有没有条目」只问一次,四个分支共用:
const nothing = !Array.isArray(hits) || hits.length === 0
default 用「nothing was found, so treat this as "unknown", not "absent"」——
把「没搜到 ≠ 没有」这条口径落在**兜底分支**上,
而不是每加一个模式才记得补一次。
W11f 判据:表驱动、双向
遍历 [undefined, no_embedder, embed_error, aborted, vector_ready_unused,
future_mode_a, future_mode_b]
零命中:notice 非空时不得含 matches below|below are|entries below
有条目:notice 非空时必须指向条目,且不得说 found nothing
第二条以前完全没人管 —— W11/W11c 只查「出现 FAILED / cancelled」,
不查它是否指向了真实存在的条目。
两条变异各自独立(实测,不是「都红就算数」)
M-A 只删 aborted 分叉
not ok 57 - W11f
not ok 58 - W11f†
ok 59 - W11f†b ← default 那半仍绿
M-B 只删 default 分叉
not ok 57 - W11f
ok 58 - W11f† ← aborted 那半仍绿
not ok 59 - W11f†b
若两条变异下红的是同一批,那就是重复计数(W11d†a/†b 的老毛病)。
验证
- npm test 118/118
- npm run check 10/10
- 连跑 8 次全量,0 失败(按你的要求超过 5 次)
- 变异 runner 零残留:/tmp/mut-?????? 前后均为 0
(/tmp/mut-probe 是 10:17 的历史副本,与本轮无关,未动)
未 amend / 未 force push,80d188e 之后是新提交。
|
2026-10-30 14:00:07 +08:00 |
|
|
|
80d188efc1
|
fix(memory-b): P0 no_embedder 逐轮注入 + 变异改为真改 lib/(并修掉自引入竞态)
P0 —— no_embedder 每轮注入(随 82b7546 进了远端)
复现(真链路,config 不给 embed = phase A 出货默认,10 个普通回合)
10/10 回合被注入,共 2130 字符,均值 213 字符/次
注入原文是恒定、无条目的两行记忆头。
根因:W11d 拆两处短路时把 no_embedder 一起放行了。
问题不在「该不该说」,在「不该每轮说」——它是一条永久配置事实,
renderIndex()(systemPrompt.context)已经每轮在讲。
代价不只是 token:每轮一段恒定无条目的记忆头,模型的正确反应是学会跳过
该段,等真降级出现时也不看,正好废掉 W11 的用意。
修法
- no_embedder 返回 ''(注释写明理由与被测数字)
- 顺手修文案矛盾:embed_error + 零命中原来会说「matches below are…」
但下面没有条目。现在分两种措辞:
零命中 -> "FAILED … and the lexical fallback found nothing"
有命中 -> "FAILED …; matches below are lexical/symbolic only"
- aborted / embed_error 不动
判据 W11e:默认配置(无 embedder)+ 零命中 ⇒ 注入必须为 ''
修复后同一探针:10 个回合 0 个被注入,0 字符
W11e 与 W11d 互为反向守卫:真·本轮事件要说,持续状态不该每轮说。
变异纪律(你指出的第四次同型,我认)
W11d†a / W11d†b 原来是测试里自己拼的局部函数,不碰 lib/ ——
真回退下它们保持绿,只有 W11d 变红。已删除,不留作计数。
换成三条真变异:W11e† / W11d† / W11d†ctx,
都真改源码、import 变异版、断言行为翻转。
★ 改真变异时我自己引入了竞态,并被抓到
第一版 withMutatedSource 直接在磁盘上改写 lib/recall.js 再还原。
node --test 是并行跑测试文件的,别的文件正在同时 import 它。
实测 1/30 左右偶发失败,症状看起来像「断言 flaky」,其实是 harness 竞态。
已改为 withMutatedCopy:整体复制 lib/ + package.json + 软链 node_modules
到临时目录,在副本上改、在副本上 import,完全不碰共享树。
验证:连续 30 次全量 0 失败;变异锚点漂移时显式报错(不会静默通过)。
文档
- README:W11e 的理由与实测数字、变异纪律(必须改 lib/,改 test/ 不算)、
以及上面这条「变异共享状态不能与代码并发跑」的竞态记录
- docs/phase-b-criteria.md:W11e 进表、W11d 的变异列改为两条独立真变异、
★★ 变异纪律段(同型四次:W11†/W11d†a/W11d†b/W12†)
验证
- npm test 115/115
- npm run check 10/10
- 连跑 3 次 test + 3 次 check 全绿
- 另跑 30 次全量确认竞态已除(0 失败)
|
2026-10-30 13:34:18 +08:00 |
|
|
|
82b754661d
|
fix(memory-b): W11d 两处短路 + W12 abort/失败分流 + 文档订正
W11d —— 你说得对,不是一行。两处短路各能独立吃掉提示
① lib/index.js contextForStep: `if (hits.length === 0) return ''`
② lib/recall.js renderRecall: `if (!Array.isArray(hits) || hits.length === 0) return ''`
修法(按你给的形态):「有没有条目」与「有没有话要说」分开判:
const notice = degradationNotice(hits)
if (lines.length === 0 && notice === '') return ''
不写成「notice 非空就总出 header」——那样健康路径零命中回合每轮灌一行记忆头,
白烧 token 还教模型忽略这一段。
四场景实测(真 Context/SystemPrompt/ToolRuntime/真 adapter/真 http)
A 降级+命中 -> empty=false failed=true entries=true 54ms
B 降级+零命中 -> empty=false failed=true entries=false 51ms ← W11d
C1 健康+命中 -> empty=false failed=false entries=true 7ms
C2 健康+零命中-> empty=true failed=false entries=false 2ms ← 正确沉默
与你实测四条一致。
W11d 判据 + **两条独立变异**
W11d†a 只把 renderRecall 短路改回去 -> not ok 50 - W11d
W11d†b 只把 contextForStep 短路加回来 -> not ok 50 - W11d
两条分别实测,证明「只修一处无效」,也证明每处都有人守着。
W12 abort 与 embed_error 分流(你上一条四项里的 ③)
原来两者都报 embed_error ⇒ 用户主动取消时提示「搜索失败」,会诱导无谓重试。
现在:options.signal.aborted 或 AbortError ⇒ 'aborted',其余 ⇒ 'embed_error'。
degradationNotice 新增 aborted 分支,措辞是「was cancelled」而不是「FAILED」。
W12 判据:两种标签、两种文案、必须不同。
变异(合并成 'embed_error')→ W10b / W11c / W12 三条同时红。
已同步 W10b/W11c/W11d 的断言(它们原假设 embed_error)。
② README:224 订正
`recall('etcd 端口', 5)` 已失效——第二形参变成 options 后,数字不是 {topK},
默认值生效。改为 `recall(q, { topK: 5 })`,并补 await(commit/recall 都是 async),
另加一段说明这个静默失效,以及「不 await 也能写」正是 W6 守着的脆弱假设。
④ docs/phase-b-criteria.md 补 W 组(原 0 命中)
新增 §W 表:W1/W5/W4/W6/W7/W8/W9/W9b/W10/W11/W11d/W12,
每条写「逼出它的 bug」与变异,并记下这组的共同教训:
判据用替身/短路/自造形状时,验的是替身而不是真代码。
(W9 假 embedder、W11d 两处短路互相掩盖、V3b 自造 2 段 batch 是同一形态。)
★ 你那条附带发现我也记下了:探针把成功响应写成 {vector:[...]},
而适配器只认 {embedding} 或 {data[0].embedding}(lib/embed/http.js:199-202),
于是「健康路径」实际一直在走 embed_error,C1 误报 FAILED。
自造 mock 时形状必须照适配器真读的字段抄,否则验的是自己的 mock。
验证
- npm test 113/113(108 + W11d/W11d†a/W11d†b + W12/W12†)
- npm run check 10/10
- 提交前连跑 3 次 test + 3 次 check,6 次全绿
|
2026-10-30 11:24:17 +08:00 |
|
|
|
72f02b866a
|
fix(memory-b): W10 读路径取消 + W11 降级可见
W10 读路径完全没有取消(真 bug,与 W9 同形)
复现(真 adapter + 挂死的本地 http,timeoutMs 压到 2000,50ms abort)
recall_elapsed_ms = 2004
decision messages = 2 (aborted after 50ms)
in-flight provider calls = 1
生产口径 DEFAULT_TIMEOUT_MS = 30_000 ⇒ 每次 pre-step 都可能卡 30 秒,
用户取消毫无作用。
根因链
lib/recall.js:437 embedText(String(query ?? '')) ← 只有一个实参
lib/index.js:237 recall(query, topK) ← 没有 signal 形参
lib/index.js:257 contextForStep(messages) ← 没有 signal 形参
lib/index.js:431 payload.signal 只被读了 .aborted 一次,之后不传
修复:signal 逐层透传
agent/pre-step payload.signal → contextForStep(messages, signal)
→ recall(query, {topK, signal}) → recallBlocksHybrid(..., {signal})
→ embedText(text, options.signal)
修复后同一复现脚本:recall_elapsed_ms = 54
W10 判据(走真 hook 链,不是直调 seam)
W10 真 adapter + 挂死 server + 50ms abort 走 pre-step ⇒ <500ms,
且 degraded === 'embed_error'
W10b 同一件事在 seam 层,防止将来重构 hook 链把回归藏起来
W10† 变异(删掉 options.signal 实参)
not ok 44 - W10: aborting a pre-step interrupts the recall embedding promptly
error: 'an aborted pre-step must not wait for the embedding provider; took 30001ms'
not ok 45 - W10b
恢复后 # pass 108 / # fail 0
★ 30001ms 正是生产口径那个 30 秒
W11 降级必须能被模型看见(你提醒的口径)
确认属实:renderRecall 原来只拼 header + 命中行,完全不渲染 degraded。
而且 hits.degraded 是 enumerable:false(A 组 golden 比较需要),
所以它对 JSON.stringify / Object.keys / 任何序列化日志都不可见 ——
断言这个属性等于什么都没证明。文本是模型唯一的信道。
新增:
- renderRecall 渲染降级提示行
- degradationNotice(hits) 导出,三分支(no_embedder / embed_error /
vector_ready_unused=不打扰模型),default 兜底防未来新模式静默落到 ''
- embed_error 的措辞点明「此处没有记忆不等于没记录过」,因为降级时
「没有证据」不等于「证据为无」
W11 判据
W11 降级 ⇒ 渲染文本含 FAILED 与 not prove it is unrecorded,
并顺带断言 JSON.stringify 确实看不见 degraded(说明为什么需要文本信道)
W11b 健康召回不出现任何提示(不误报)
W11c 端到端:aborted pre-step 的文本既有命中也有提示,且 <500ms
W11† 变异(renderRecall 去掉提示行)
not ok 46 - W11
not ok 48 - W11c
恢复后 108/108
一处自我更正:W11c 初版没有任何实质断言(注释里写「text 可能为空,
所以只断言类型」),那是假测试。已改为种子一条符号路可命中的块,
断言文本非空、含 FAILED、含命中值、且 <500ms。
★ 顺带记录一次操作事故:改 renderRecall 时我的 Python 替换把整个
文件匹配进去,lib/recall.js 被写成 53 MB。已 git checkout 恢复并重做,
当前文件 521 行、syntax OK、全量测试通过。以后大段替换先备份。
验证
- npm test 108/108(102 + W10/W10b + W11/W11b/W11c)
- npm run check 10/10
- 提交前连跑 3 次 test + 3 次 check,6 次全绿
|
2026-10-30 11:01:22 +08:00 |
|
|
|
170bc5fde2
|
fix(memory-b): W9 —— adapter 的 embedText 丢掉 caller signal(真 bug)
复现(真 adapter + 本地真 http server,3s 回包,50ms abort)
signal forwarded? outcome=RESOLVED vector=[0.1,0.2,0.3] elapsed=3012ms
★ caller abort ignored — signal dropped by embedText signature
根因:lib/index.js:173 调 embedText(text, options.signal),而
lib/embed/http.js:152 声明的是 embedText(text) —— 第二实参被 JS 静默丢弃。
生产语义:用户已取消,工具继续阻塞到 adapter 自己的 30s 超时,
而且最后还把向量写进库;isConcurrencySafe 的兄弟调用一起被堵。
为什么 99 条全绿
W5/W5† 用的是手写慢 embedder,它自己 addEventListener('abort') 了 ——
判据测的是「我希望的行为」,没测真实 adapter。与上一轮 V3b 自造
2 段 batch 是同一形态:替身站到了唯一可能出错的那段代码前面。
修复
- embedText(text, signal):用 AbortSignal.any([signal, controller.signal])
融合两个取消源。两个语义都保留:
· 用户取消要立刻停(实测 50ms -> 53ms)
· provider 卡死也要停(实测无 signal 时 151ms 超时;
有未取消的 caller signal 时仍 151ms 超时)
Node >= 22.5 已有 AbortSignal.any,不手写 fuse、不轮询。
- 同步修 Embedder 的 JSDoc 与 interface 类型,把第二个参数写进契约
W9 判据(真链路,不用假 embedder)
W9 caller abort 打断真实请求:耗时 < 500ms 且确实抛出
W9b adapter 自身 timeout 在有 caller signal 时仍然生效
W9c 端到端(真 server + 真 adapter + ctx.tools.execute):
块仍写入、vector 为空、error.info.code === 'ABORTED'
W9† 变异(贴红绿原文)
★ 第一次变异不干净:只改签名,函数体仍引用 signal ⇒ 抛 ReferenceError
⇒ W9 看到「很快抛错」判为通过。这是假绿。
干净的变异(签名单参 + 去掉 fuse):
not ok 41 - W9: a caller abort interrupts a REAL http embedding call promptly
an aborted call must reject, not resolve with a vector
not ok 43 - W9c: end-to-end — aborted tool call still writes the block
恢复后 # pass 102 / # fail 0
f. 同形问题全仓扫描结论(grep + 实参/形参比对,非「我觉得」)
lib/ 里所有传到 Embedder 的调用点:
lib/index.js:173 embedText(text, options.signal) -> 形参 (text, signal) ✔
lib/recall.js:434 embedText(String(query ?? '')) -> 单参,合法 ✔
自动比对脚本报了一处 withDiagnosis 4>3 的告警,手工核对为误报:
6 处调用实参分别为 2/2/3/3/3/2,正则被 Object.defineProperty 干扰。
结论:无其它「传了实参、对面签名不收」的调用点。
README:376 修正
原文写「exec.signal is forwarded into embedText」——参数确实传了但对面对掉,
比没传更糟(读文档的人会以为验过)。改为与代码一致的表述,
并注明「W5 只覆盖管道,适配器侧由 W9 覆盖」。
另记下变异必须干净这条:失败原因不对的变异不算证据。
验证
- npm test 102/102(99 + W9/W9b/W9c)
- npm run check 10/10
- 提交前连跑 3 次 test + 3 次 check,6 次全绿
|
2026-10-30 10:41:42 +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 |
|
|
|
2adcbb7387
|
feat(memory-b): 票② 单一来源强制 + 票③ dim=null 边界
票② 采纳,并补上代码层的强制(原实现只是测试碰巧传同一串)
之前 putBlock 与 saveCentroid 各自接受一个 fingerprint 参数,
测试恰好传同一个值,但插件本身没有任何机制阻止调用方各自重新派生。
现在改为结构性保证:
- StableMem.space() 是 fingerprint 的唯一来源(返回 embedder.info())
- commit() 自己调 embedText,并用 space().fingerprint 盖戳;
调用方无法传入第二个拼写
- saveSpaceCentroid() **不接受 fingerprint 参数**,只能取 space() 的
- info() 在 adapter 内 memo 一次,进程内不重算(票②第三条约束)
- commit 在无 embedder 或 embedder 失败时仍写块(vector 留空),
不编造零向量;工具返回里会标注 "(stored without a vector)"
判据(按你给的最低验收:集合断言)
ticket②-strong: 服务路径写 3 块 + saveSpaceCentroid
→ SELECT DISTINCT fingerprint FROM memory_blocks ≡ graph_centroid 键
→ 且 loadCentroid(块侧串) 能查到
ticket②-strong mutation: 中心侧用另一种拼写(?tenant=b)→ 块侧串查不到,
且不抛错
ticket②-strong: 规范化把 `…/embed` 与 `…/embed/` 归为同一空间
变异(/tmp 干净副本)
中心键加后缀 "-x" → not ok 55 - ticket②-strong
commit 用 text.length 派生 → not ok 55 - ticket②-strong
票③ 边界:未知维度必须是 null,不能是 0
ticket③: 有向量块、无中心行 → loadCentroid 返回 undefined(不抛)
→ 诊断投影 dim === null,且断言 != 0
ticket③: 有中心行时才报数值维度(来自中心元数据,不是 length(vector))
ticket③: 缺中心不改变召回行为(recallBlocksHybrid 仍走符号路;
recallBlocks 仍同步)
变异(/tmp 干净副本)
loadCentroid 缺行时伪造 dim:0 → 4 条判据红
loadCentroid 缺行时抛错(HA 视为正常降级) → 同样 4 条红
按你三点落地约束执行,未越界:
- 四态计数(noVector/unknownSpace/spaceMismatch/embedFailure)留给 F/D,
H 只提供窄 Embedder(embedText/info/close)+ schema + 迁移
- 未把 V/C 融进召回:recallBlocks 同步签名与行为未动(A1/A1b 仍绿)
- 旧库迁移保留且可重入:MIG1/MIG2/MIG3/MIG3b/MIG4 全绿,
MIG3 仍在 npm run check 里(10/10),未覆盖 phase-A 存量块
验证
- npm test 90/90(84 + 票②三条 + 票③三条)
- npm run check 10/10
- 提交前连跑 3 次 test + 3 次 check,6 次全绿
- README 的 check 输出块重新粘贴真实输出(漂移守卫仍绿)
|
2026-10-30 10:32:04 +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 |
|
|
|
afc98191a9
|
fix(memory-b): MIG3 graph_centroid 迁移后未重建(P0)+ P1 改重命名保留
⚠️ 顺序说明:本提交修的三条都在 f84011d 里已经发出去了。
你核的是 f84011d 之前的未提交工作区;H 组内容与迁移我都已推送。
所以 f84011d 本身带了这个 P0(phase A 库迁移后写不了中心表)。
不掩饰,记在这里。
P0:迁移把 graph_centroid 整张表弄丢
复现(三个库,结果与你完全一致)
A) phaseA 库(旧 centroid,空表) → opened, uv=1, putBlock OK,
saveCentroid THREW: no such table: graph_centroid
B) phaseA 库(旧 centroid,1 行) → open() THREW: refusing to migrate ...
C) 全新库 → saveCentroid OK
根因:open() 里 DDL 先跑、migrate 后跑。DDL 对已存在的旧表是 no-op,
migrate 里 DROP TABLE 之后再也没有东西重建它。
A 是最糟形态:开得了、stamp 了 uv=1、能写块、FTS 能召回,
只有第一次 saveCentroid 才炸 —— 失败窗口最窄,所以 MIG1(只看
memory_blocks)结构上扫不到。
修法
- 把中心表 DDL 抽成 DDL_CENTROID,在 migrate 里 DROP/RENAME 之后
就地 db.exec(DDL_CENTROID) 重建(不是靠前面那次 DDL)
- 新增 BlockStore.tables() 与 raw()(后者仅供测试做行数断言)
P1:B 分支不再硬失败,改重命名保留
原来 throw 让整个库打不开,而错误信息还指向一个不存在的出路
("export the data",但 phase A 从不写 centroid、也没有导出工具)。
裁定改为:有行则 RENAME 成 graph_centroid_phase_a_<ts>,启动继续、
数据不丢、旧行仍可用 sqlite3 查;空表则 DROP 后重建。
你说「如果认为保留旧表会造成 pragma_table_info 误判,说明理由再定」——
不会:命名带 _phase_a_ 前缀,且 migrate 只检查名字恰为
graph_centroid 的那张表。
MIG1 mutation 已删除(它冒充变异)
那条手工构造 raw DB、直接对 DatabaseSync 断言,一行 lib 代码都没碰,
永远不会随实现回滚变红。是演示不是守卫。已删,并在原位置留下说明:
真正的守卫是 MIG1 正向那条(注掉 ADD COLUMN vector 即红)。
判据与变异(全部在 /tmp 副本上做,按你的纪律)
MIG3 phase-A 库 → 列齐 → saveCentroid 成功 → loadCentroid 读回
MIG3b phase-A 库(1 行)→ 能打开、新表可用、旧表重命名且行数保留
MUT-A 删掉重建 DDL(原 P0 形态)
not ok 24 - MIG3 error: 'dsh-memory: no such table: graph_centroid'
not ok 25 - MIG3b
(MIG1/MIG2 仍绿 —— 精确复现「MIG1 看不到这张表」)
MUT-B 有行改回 throw
not ok 25 - MIG3b
MUT-C 注掉 ADD COLUMN vector
not ok 49 - MIG1
MUT-D 完全去掉 migrate()
not ok 49/50/51 - MIG1/MIG2/MIG3
验证
- npm test 82/82(81 - 1 条假变异 + MIG3/MIG3b 两条)
- npm run check 9/9
- 提交前连跑 3 次 test + 3 次 check,6 次全绿
- 所有变异在 /tmp/mvp-copy 副本上执行,未在共享树里改 lib
待办
- 票③ D7 fail-closed(中心表空但有向量行 ⇒ dim 报 null)跟 F 组
|
2026-10-30 10:22:25 +08:00 |
|
|
|
f84011d9ef
|
fix(memory-b): phase A 存量库迁移 + 成功路径 degraded 标签断言
⚠️ 先说明顺序问题:H 组我已在 00a4a5f 提交并推送,在你这条指令之前。
你要求「迁移是 H 的前置」,我同意,但已经发出去了。
本提交补上迁移,00a4a5f..本提交 之间是一个真实的破窗状态
(存量库会被打成只读),已记在这里,不掩饰。
阻塞① 迁移(硬 bug,我复现了)
复现:用 phase-A 形库(无 vector/fingerprint 列,user_version=0)
opened OK ← 加载不报错
PUTBLOCK THREW: table memory_blocks has no column named vector
blockFingerprints THREW: no such column: fingerprint
根因:DDL 全是 CREATE TABLE IF NOT EXISTS,对已存在表静默 no-op。
实现(lib/store.js)
- 新增 SCHEMA_VERSION = 1 与 migrate(db)
- 探测而非猜:读 pragma_table_info('memory_blocks'),缺哪列 ALTER 哪列,
不靠 try/catch 吞异常(半个迁移被吞掉是最难查的形态)
- user_version 为 0 不等于缺列(phase B 崩溃前未盖戳的库会是这样),
所以判据是列存在性,不是版本号
- graph_centroid:phase A 的空表直接 DROP 重建;有数据则**显式拒绝并报错**,
不销毁数据
- 新增 userVersion() 供读取
- open() 在 DDL 之后调用 migrate():新库已具当前形状,只有老库有活干
判据
MIG1 phase-A 库:列补齐 → 老块 getBlock 读得到 → FTS 仍命中
→ putBlock 成功 → user_version==1 → 重开幂等
MIG2 新库直接是 version 1
MIG1† 不迁移时那条真实写入必须失败(并断言 phase-A 表确实无 vector 列)
变异打红原文(把 migrate(db) 注释掉)
not ok 21 - MIG1: a phase-A database gains the vector columns and stays writable
error: 'missing vector column; have id,modality,text_content,source,scene,semantic_type,created_at,updated_at'
not ok 22 - MIG2: a fresh database is created at version 1
恢复 → # pass 23 / # fail 0
★ 我第一版 MIG1† 写错了:用了一条不提到 vector 的 INSERT 去断言会失败,
而那种 INSERT 本来就会成功。已改成 phase B 实际执行的那种写(显式带
vector/fingerprint),并加断言确认 phase-A 表确实没有该列。
阻塞② 成功路径的 degraded 标签
你复现过:把 lib/recall.js 的 'vector_ready_unused' 改回 'hybrid',
76 条全绿 —— 成功路径的标签没有任何断言。已补:
A3c: 健康 embedder ⇒ degraded === 'vector_ready_unused',
且断言 != 'hybrid',并确认 embedText 真被调用了一次
A3c mutation: 交换常量必须抛 ERR_ASSERTION
真实代码变异(把标签改回 'hybrid',跑全量)
not ok 12 - A3c: a healthy embedder reports "vector_ready_unused", NOT "hybrid"
error: group A computes the vector but does not score with it — the label
must say that, because group V will need a different label once
vectors really participate
expected: 'vector_ready_unused'
# tests 81 # pass 80 # fail 1
恢复 → # tests 81 # pass 81 # fail 0
端到端复验(真实 phase-A 库过一遍插件)
loaded on phase-A DB, user_version = 1
commit: Committed block blk_5e3653f55544.
legacy block recallable: true -> 值班室分机号 4324
验证
- npm test 81/81(76 + MIG 三条 + A3c 两条)
- npm run check 9/9
- 提交前连跑 3 次 test + 3 次 check,6 次全绿
待办
- 票③ D7 fail-closed(中心表空但有向量行 ⇒ dim 报 null)跟 F 组
|
2026-10-30 10:20:10 +08:00 |
|
|
|
00a4a5f6ee
|
feat(memory-b): G2† 编成 test + H 组完成(http adapter + 两表指纹一致)
G2† 补成自动化(你坚持的那条,我认)
你指出 G 组只有三条变异是自动化断言,G2† 只活在我的 commit message 里 ——
手工记录的变异不会随时间复现。已编成 test,并扩到覆盖**每一条**降级路径:
DEGRADATION_PATHS 数据表(6 条,新增第五条时未覆盖则测试即红)
embedder is null / undefined / 无 embedText
★ 无 info() ← 你点名的那条
★ info() 的 fingerprint 为空串
★ info() 的 fingerprint 非字符串
+ 每条都断言 degraded=no_embedder、仍能召回、且零 fetch
+ 额外一条「指纹检查在 embedText 之前」(钉顺序,不只钉结果)
变异实测打红
把 embedText 调用挪到指纹检查之前
not ok 8 - G2†: the fingerprint check runs BEFORE embedText is called
让无指纹分支发一次 fetch
not ok 5 - G2†: "embedder with no info()" ... zero network calls
H 组实现(lib/embed/http.js,单文件,G5 守卫)
- 协议与 HA 逐字段一致:model / modality / side / text;响应两种形状都收
- 指纹 http:<model>:<dim>:<sha256(规范化 endpoint)[:8]>
规范化(hash 之前):去空白 / scheme+host 小写 / 去尾斜杠 / 去默认端口
不做 query 重排与 .. 解析(那些差异通常是真实的)
★ 构造时算一次并复用 —— 因为同一个串必须同时进两张表
- 失败在 adapter 层是硬错误(非 2xx / 无 embedding / 维度不符 / 超限 / 超时),
在召回层被 catch 成 degraded='embed_error',退回 FTS5
- 维度不符绝不截断不填充(block_recall.go:135-138)
票② 两表指纹一致 —— 落在 H 组 schema,未拖到 F
- memory_blocks 加 vector / fingerprint 两列(phase A 的表已发布,
这里用 CREATE TABLE IF NOT EXISTS 的新列,旧库需 ALTER;已记入待办)
- graph_centroid 列名对齐 HA(vector_count / vector / stats / built_at),
并把 is_anomalous 提成独立列(票③口径),不塞进 stats JSON
- putBlock 拒绝「有向量但无指纹」——无标签的向量永远不可安全比较
- putBlock 重提交不带向量时不清空已有向量(与 PutMemoryBlocks 的空值不覆盖同构)
- 新增 saveCentroid / loadCentroid / blockFingerprints
票② 判据
ok - ticket②: block fingerprint and centroid fingerprint are byte-identical
ok - ticket② mutation: a re-derived centroid fingerprint misses the lookup
(加一个尾随空格即查不到,且不抛错 —— 正是必须用断言钉住的原因)
ok - ticket②: a vector without a fingerprint is refused at write time
ok - ticket②: re-committing without a vector must not erase the stored one
H 组变异(8 条,全部实跑打红)
HM1 side 写死 document → not ok 1 - H1
HM2 指纹去掉 endpoint hash → not ok 10/12/13 - F-fp1/fp3/fp4(三条一起红)
HM3 维度不符改为截断 → not ok 5 - H3b
HM4 去掉响应体积上限 → not ok 9 - H6
HM5 保留尾斜杠 → not ok 11 - F-fp2
HM6 非 2xx 当成功 → not ok 4 - H3
HM7 忽略 timeout signal → not ok 8 - H4
HM8 指纹每次 info() 重算 → not ok 15 - F-fp6
一处设计确认:lib/index.js 现在 import './embed/http.js',
G5 仍绿 —— 因为该文件没有 fetch/Authorization 等 transport 标识符。
「可以 import adapter,不可以知道 transport」正是 G5 的设计意图。
验证
- npm test 76/76(56 + H 组 20)
- npm run check 9/9
- 提交前连跑 3 次 test + 3 次 check,6 次全绿
待办(不催,记着)
- 票③ D7 fail-closed(中心表空但有向量行 ⇒ dim 报 null 不报 0)跟 F 组
- phase A 旧库升级需 ALTER TABLE 加两列,随 D 组的 stats 工具一起做
|
2026-10-30 10:17:18 +08:00 |
|
|
|
e08c0bc569
|
fix(memory-b): G5 白名单恒假 —— 加合法 adapter 会误炸
你抓的这条是真 bug,属"绿着但错"。
根因
test/phase-b-g-static.test.js 里
const ADAPTER = 'lib/embed/'
if (rel.startsWith(ADAPTER.slice(4))) continue // slice(4) === 'embed/'
而 rel 是 'lib/embed/http.js' ⇒ startsWith('embed/') 恒 false
⇒ continue 永不执行 ⇒ 白名单等于不存在。
它一直绿,只是因为 lib/embed/ 目录还没建,空跑。
H 组一落地,这个守卫就会对合法的 adapter 自己报红
(fetch( / Authorization / Bearer / https://),
然后最省事的下一步就是把它删掉或放宽 —— 误炸的守卫一定会被关掉。
修法
if (rel.startsWith(ADAPTER)) continue
并在注释里写清这个 bug 的形态,防止再被"优化"回去。
实测复现(按你给的场景,在你的工作区之外做的)
1. 只放一个合法 lib/embed/http.js(带 fetch + Authorization + https://)
修前:not ok 2 - G5 (报 lib/embed/http.js: fetch( / Authorization / Bearer / https://)
修后:ok 2 - G5
2. 往 lib/store.js 塞 fetch('https://x/')
修后:not ok 2 - G5,且只点名 lib/store.js
补四条变异(你要求的三条 + G2†)
你指出 G 组 5 条守卫一条变异都没有 —— 确实是疏漏,
上面那个 bug 正是没有变异的直接后果。已补:
G5† 把 startsWith(ADAPTER) 改回 startsWith(ADAPTER.slice(4))
实测:放合法 adapter 后 → not ok 2 - G5(红);
改回正确写法 → 恢复绿
G5b† lib/store.js 插 fetch('https://x/')
实测:not ok 2 - G5,报 'lib/store.js: fetch(' 与 'lib/store.js: https://',
且不点名 lib/embed/
G2b† lib/recall.js 加 import 'onnxruntime-node'
实测:模块已无法加载(LOAD FAILS: ERR_MODULE_NOT_FOUND),
而静态守卫仍独立报出 'lib/recall.js: onnxruntime-node'
—— 这正是上一轮把静态判据拆出独立文件的收益
G2† 无 embedder 路径真发一次 fetch
实测:not ok 1 - G2,报 "no embedder must mean zero network calls; saw 1"
G5†/G5b†/G2b† 的实现方式说明
它们不修改磁盘上的 lib/(判据不该改它正在验证的树),
而是把同一套检测逻辑参数化后跑合成文件表,断言"规则如所写会抓到这个"。
G5† 额外把有 bug 的前缀也实现了一遍,证明它会放过合法 adapter ——
即这次 bug 的形成机制。另三条(G5b†/G2b†/G2†)同时做了真实代码变异,
红→绿的原文见上。
验证
- npm test 49/49(46 + G5†/G5b†/G2b† 三条)
- npm run check 9/9
- 提交前连跑 3 次 test + 3 次 check,6 次全绿
票②(两表 fingerprint 字符串相等)与票③(D7 fail-closed)
跟 F 组一起落。G 组修完,H 组可以开工。
|
2026-10-30 10:12:40 +08:00 |
|
|
|
600df5b817
|
feat(memory-b): G 组完成 —— 5 条守卫 + 两条你要求的即时修正
先落你要求的两处修正(在 G 组之前)
1. A1 加硬:新增 A1b,用 `git show 885e0b6:lib/recall.js` 取冻结版,
逐字节比对 recallBlocks 的函数体区间。
实测有效:把 `String(query ?? '').trim()` 改成 `(query ?? '').toString().trim()`
(行为等价、仍同步),A2 golden fixture **仍绿**,但 A1b 红 ——
正是你指出的「挡不住顺手优化同步版」的缺口。
2. `degraded: 'hybrid'` → `'vector_ready_unused'`。
现在语义是「向量算成功了但没进评分」,叫 hybrid 是骗人的。
已同步改 README,并注明 V 组接上后再改名。
G 组判据(test/phase-b-g.test.js 行为 3 条 + test/phase-b-g-static.test.js 静态 2 条)
- G2 无 embedder ⇒ 零网络调用(替换 globalThis.fetch 为 tripwire)
- G2b lib/ 只 import node: 内建与 dsh peer
- G3 embedder 抛错时 stablemem_commit 仍写入,符号路仍能召回
- G4 只有 text 模态,memory_blocks 不存在 media 列
- G5 transport 标识符(fetch( / Authorization / Bearer / http:// /
model_dir / api_key)只许出现在 lib/embed/ 下
G 组变异(全部实跑打红,原文)
GM1 无 embedder 分支加 fetch
not ok 1 - G2: with no embedder, recall performs no network I/O
GM2 commit 先 await embedder,失败则抛
not ok 3 - G3: stablemem_commit writes even when the embedder throws
GM3 memory_blocks 加 data BLOB 列
not ok 4 - G4: only the text modality is produced
GM4 recall.js 里写 endpoint 字面量
not ok 5 - G5: provider-specific identifiers may appear in exactly one lib file
GM5 lib/store.js 加 import onnxruntime-node
not ok 1 - G2b: lib/ imports only node: builtins and cordis/dsh peers
★ G 组自己踩到的两个坑(都是判据本身的缺陷,不是 lib 的)
1. 静态扫描判据不能 import 被扫描的代码。G2b/G5 原本和行为判据放同一文件,
而那个文件 import 了 lib/index.js ⇒ lib/ 一坏整份测试崩,
报的是 "load failed" 而不是「你加了不该加的依赖」。
已拆到 test/phase-b-g-static.test.js(不 import 任何 lib/),
实测:lib/store.js 加 onnxruntime-node 后模块加载失败,
静态判据仍独立报出 G2b 红。
2. 扫描必须去注释。G5 第一版把 recall.js 注释里的「no endpoint」
和 store.js 的「edge endpoint」当成违规。误报的守卫会被关掉,
比没有更糟。已改为先剥注释再扫。
顺带:为 G4 的 schema 内省加了 BlockStore.columns(table)(公开方法),
而不是让测试去碰私有 handle。
未做的事(等你裁定后再动)
- F 组:等 §0.1 endpoint 规范化 / §0.2 四态 / §0.3 stats+is_anomalous
独立列 / §0.4 降级落地
- 已记录你指出的关键一处:HA 的指纹守卫有**两处**
(block_recall.go:140 与 fusion.go:495),同一个三连 &&,
空 fingerprint 在向量路和符号路上都会被放行。
F 组判据必须覆盖符号路,只测向量路等于没测。
我核过 fusion.go:481-495 的注释与 toolcall_block_test.go:124 的起因,
确认属实。
验证
- npm test 46/46(40 + G 组 5 + A1b)
- npm run check 9/9
- 提交前连跑 3 次 test + 3 次 check,6 次全绿
|
2026-10-30 10:09:45 +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 |
|
|
|
eac9f0fca6
|
docs: phase B 开工前决策建议 —— 异步化回归风险可结构性消除
回应四条裁定与一个交叉点。不含实现。
核心结论:草案 A1「recallBlocks 改 async」是我图省事的设计偷懒,
把「要拿向量」和「要打分」混在一个函数里。切分后风险归零:
recallBlocks(store, query, opts) 保持同步,签名行为不变
新增可选 opts.vectorScores = Map<blockId, number>
recallBlocksHybrid(store, embedder, q, opts) 新的 async 包装
实测调用面:生产代码 3 行(lib/index.js:124/140/277),
测试代码 0 行(test/store.test.js 5 处 + scripts/checkpoints.js 4 处
全部继续调同步版)。已实测确认 test/plugin.test.js:119 那个
`contextForStep = () => ''` 的同步桩在 async 化后仍兼容(await 非
Promise 值即其本身),所以那条变异测试继续有效。
建议把 guard 组 A1 改成「recallBlocks 保持同步且实现不变」,
并新增 A3:hybrid 在 embedder 为 null 时返回值与同步版逐字段相同
(含 why 字符串)——切分后最大的新风险是两个入口漂移,A3 钉死它。
四条裁定核对
- §2.1 endpoint 规范化:建议固定四条(去尾斜杠 / host 小写 /
省略默认端口 / 去空白),不做 query 重排与路径归一。
- §2.2 四态:可执行,我草案的三态漏了「向量化失败」,裁定并进来更准。
新增 D7:stats 默认不解析向量 JSON(用 length(vector)),维度从
graph_centroid 取。修正一处我原稿的说漏——HA 为算 Dimensions 必须
逐行 decodeVector(block_recall.go:303/313),所以「维度统计」与
「不解析」互斥,只能靠中心表绕开。
- §2.3 centroid 补 stats 列:已列为第一件事。
- §2.4 失败降级:同意,并建议 mode 三值而非 bool
(no_embedder / embed_error / all_mismatched 对用户意义不同)。
§3 建议把 embedder 焊成单方法窄接口(embedText / info / close),
加 G5 源码 grep 守卫:除 lib/embed/http.js 外不得出现 http/endpoint/
fetch/Bearer 等 provider 专属标识——机器可查,比约定强。
§4 交叉点:建议投完整 phase B,按 §1 切分后 5.5-6.5 人天。
但建议你先确认一件事:是否有真实使用中的 dsh 会话在产生块?
若无,可先投 2.5 人天的中间态(向量写入 + 四态可见,不改召回主路径)。
§6 记录本轮两处自我修正。
参考仓未改动。
|
2026-10-30 09:52:06 +08:00 |
|
|
|
0dfa6dcb27
|
docs: phase B 判据与变异规则草案(待批,不含实现)
decision 1(http 转发、不背 ONNX)与 decision 3(跳过不报错但失配计数
必须暴露到工具面)已落实到判据;decision 2 未回,全文按草案状态。
开工前必须先定的 4 件事(各附我的建议与代价):
§0.1 ★ HA 的 http 指纹派生式漏了 endpoint
http_embedder.go:99 只取 "http:"+model+":"+dim。
换 endpoint(换机器/A-B 服务/换 region)而 model 同名同维时,
指纹相同 ⇒ 静默混用两个向量空间。
建议扩展为含 endpoint hash;代价是换 endpoint 触发一次全量重嵌。
§0.2 ★ 空 fingerprint 的带向量块会被静默放行
block_recall.go:140 的跳过条件是三个 && ⇒ fp 为空时不跳过。
phase A 的块全都无向量,phase B 回填后「无向量」与
「有向量但指纹空」是两种不同状态,混在一起用户无法知道怎么修。
建议三态归类 + fail-closed(no_vector / no_fingerprint / stale / active)。
§0.3 graph_centroid DDL 与 HA 不一致(我的疏忽)
少了 stats 列,而 HA 靠 meta.Anomalous 决定要不要启用中心化
(centroid_store.go:205-209)。现在表是空的,改列名零成本。
§0.4 embedding 调用失败是否一并进计数器(与 decision 3 同构,建议纳入)
判据 6 组共 34 条(A 前置重构 / F 指纹空间 / V 融合 / C 中心化 /
D 降级可见性 / H http provider / G 守卫),每条带变异。
变异规则汇总出 12 条必须能红的,其中 M-V1(精确串从布尔降成权重)
是 HA 真实踩过的 bug(0.300 输给 0.534)。
另外三项判断:
- 增加 golden fixture 固化 phase A 召回结果,作为「加向量没顺手改符号路」的守卫
- memory_blocks 要加 vector/fingerprint 两列,phase A 的 DDL 已发布 ⇒
建议 phase B 第一件事补 PRAGMA user_version 迁移框架(现在 0→1 一步,
一天成本;等 L2 落地再补就贵了)
- 工作量从原估 3-4 人天上修到 6-7 人天(原估漏了 schema 迁移、
stablemem_stats 工具、异步化带来的全量测试改动)
不含任何实现。参考仓未改动。
|
2026-10-30 09:49:07 +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 |
|
|
|
533f801c24
|
docs: 影子上下文方案在 dsh 上的可行性核实
结论:路线②(注册独立 agent 模式)可行;路线①(劫持原有信息流)
只是看着可行。
路线②可行 —— agentPresets 是一等机制
- ctx.agentPresets.register({id, plugins}) 可由任意插件调用
(dsh-agent-preset/lib/index.js:18-26)
- preset 子树是独立 scope + 可选 isolate realm
(dsh-agent-preset-registry/lib/types/mount.js:183-198)
- 已有实证:standard.patch.yml:66-82 的 isolate:{compaction:true}
group 已经在替换 compaction 服务
- 因此「压缩时机」「不压缩」「压缩前抓原文」全部可控
路线①为什么看着可行
- agent/pre-step 看着能注入,其实注入即落盘:
loop 随后 session.append("user/message", ...)
(dsh-agent-loop/lib/index.js:1067-1073),注入内容成为真实历史
- llm/stream 看着能改最终请求,其实 loop 请求深冻结
(dsh-llm/lib/types/index.d.ts:37-42),mutation throws
- 「不装 compaction-basic」看着能旁路,其实 auto:false 会连
溢出恢复一起关掉 (dsh-compaction-basic/lib/index.js:827)
关键机制核实
- compaction 自动触发只是两个 ctx.on 监听器
(dsh-compaction-basic/lib/index.js:839/:861),受 config.auto 控制
- 服务名同 realm 撞名抛错 (cordis/lib/index.js:814),换实现必须 isolate
- surface 支持整段 replace,但被遮蔽节点必须全部列进 sourceEventSeqs
(dsh-session/lib/index.js:314-332)
- 日志 seq 严格连续 (dsh-session/lib/index.js:426),影子副本只能活在
dsh 之外
- MessageSourceMap 可 merge 扩展,自定义字段 lossless-JSON 存活但不进
wire(adapter 只投影 role/content)
- setFactory 独占 (dsh-agent/lib/index.js:431 + agent-loop:1590),
自建 runner 需弃用 2023 行的 agent-loop
三处与路线无关的硬阻塞:surface 不能插中间、llm/stream 深冻结、
前缀时序只能靠末尾追加。
人天:路线① 12-18(仅 60% 子集);路线② 18-26;混合 22-30。
附完整证据索引(6 组,全部 lib/*.js 实现级核对)。
原文只读,未修改 HologramAgent 仓库。
|
2026-10-30 09:29:37 +08:00 |
|
|
|
28d9d5e929
|
docs: 三层记忆移植 dsh 插件可行性评估
结论:只能做子集。
逐层结论
- L1 (context 热上下文) ≈20%:dsh 的上下文压缩是「摘要替换区间」
(dsh-compaction-basic 的 compactSurfaceRegion),不是「按 query 相关性
淘汰 + 迁出到冷层 + 按原时间戳迁回」。CompactionTrigger 接口拿不到
query;MessageBase 无 timestamp 字段,surface 是 append-only + replace,
无法语义插入。
- L2 (coldcontext 冷文档) ≈80%:存储/召回/注入可落,缺
「Peek → 回灌 → Remove」的回收站语义。
- L3 (stablemem 图记忆) ≈85%:memory_blocks/memory_block_edges 结构
完全可映射,node:sqlite 实测可用(含 FTS5);受限于无 embedding 服务。
关键发现
- dsh 无任何 embedding/vector service,需自建(推荐复用 HA 的 http
provider HTTP 协议转发,2 人天)。
- systemPrompt.context() 的 AssembleContext 拿不到本步消息正文,
「按本次输入召回后注入」必须走 agent/pre-step waterfall
(参考 dsh-time-context 的用法)。
- dsh attachment 是 CAS,与 HA「媒体字节内联在块里、不做引用账本」
的设计冲突 → 媒体块必须裁掉。
工作量(含判据与实测)
- L2+L3 子集:36-53 人天
- MVP(L3 + 工具 + 注入面):19-25 人天
- 完全等价(含迁出-迁回):不可行,不报价
报告含 7 章 + 两份附录(HA/dsh 双侧证据索引、环境实测结果)。
原文全程只读,未修改 HologramAgent 仓库。
|
2026-10-30 00:39:44 +08:00 |
|
|
|
16fa18a186
|
Initial commit
|
2026-10-29 16:27:48 +00:00 |
|