@modusensus/dsh-mneme 0.8.11 → 0.8.13
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 +5 -5
- package/assets/icon.svg +3 -3
- package/lib/api.js +2 -0
- package/lib/client.js +12 -1
- package/lib/config.js +66 -1
- package/lib/dream/decisions.js +16 -0
- package/lib/dream/sleep.js +3 -0
- package/lib/dream.js +57 -9
- package/lib/entities/extractor.js +4 -1
- package/lib/graph/anchoring.js +180 -0
- package/lib/guide.js +13 -1
- package/lib/index.js +18 -5
- package/lib/inject.js +15 -3
- package/lib/lang.js +10 -0
- package/lib/sensitive-scan.js +209 -0
- package/lib/service.js +203 -35
- package/lib/settings.js +29 -3
- package/lib/store.js +155 -11
- package/lib/summarize.js +104 -7
- package/locale/en.json +6 -6
- package/locale/zh.json +6 -6
- package/package.json +114 -113
- package/scripts/sync-test-badge.mjs +9 -7
- package/scripts/test-count-sync.mjs +44 -0
- package/src/api.js +2 -0
- package/src/config.js +66 -1
- package/src/dream/decisions.js +16 -0
- package/src/dream/sleep.js +3 -0
- package/src/dream.js +57 -9
- package/src/entities/extractor.js +4 -1
- package/src/graph/anchoring.js +180 -0
- package/src/guide.js +13 -1
- package/src/index.js +18 -5
- package/src/inject.js +15 -3
- package/src/lang.js +10 -0
- package/src/sensitive-scan.js +209 -0
- package/src/service.js +203 -35
- package/src/settings.js +29 -3
- package/src/store.js +155 -11
- package/src/summarize.js +104 -7
- package/test/api.test.js +15 -2
- package/test/baseline-persist.test.js +248 -0
- package/test/dream.test.js +75 -0
- package/test/graph-anchoring.test.js +185 -0
- package/test/graph-inject-budget.test.js +104 -0
- package/test/graph-passive-confirm.test.js +92 -0
- package/test/graph-weight-evolution.test.js +105 -0
- package/test/inject-pools.test.js +19 -0
- package/test/scope-inject-soft.test.js +106 -0
- package/test/scope-strict.test.js +112 -0
- package/test/sensitive-scan.test.js +207 -0
- package/test/summarize.test.js +177 -0
- package/test/test-count-sync.test.js +60 -0
- package/test/write-admission.test.js +115 -0
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
8
|
-
[](https://github.com/slow-stack/mneme)
|
|
9
9
|
[](https://github.com/slow-stack/mneme/actions)
|
|
10
10
|
[](https://nodejs.org)
|
|
11
11
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
@@ -253,7 +253,7 @@ v0.3.0 起新增**记忆基因**层:从记忆里抽取**命名实体**、**带
|
|
|
253
253
|
|
|
254
254
|
### 前置条件
|
|
255
255
|
|
|
256
|
-
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)— 兼容 DSH 0.1.x
|
|
256
|
+
- [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)— 兼容 DSH 0.1.x 与 0.2.x(三段式 peer 范围见 `package.json`);开发与 CI 已对齐 0.2 线,`0.2.0-rc.1` / `0.2.0-rc.2` 实测全绿。旧版兼容垫片(`Session.events` → `snapshotEvents()`,v0.7.9 起确保进入发布产物 lib/)保留
|
|
257
257
|
- Node 24+(`node:sqlite`)
|
|
258
258
|
|
|
259
259
|
### 安装步骤
|
|
@@ -430,7 +430,7 @@ dsh web
|
|
|
430
430
|
| `searchSemanticDedupThreshold` | `0.95` | 语义去重相似度阈值(v0.5.0,默认 0.95,范围 0.5-1.0):`searchSemanticDedup=true` 时生效,调整可防小模型误折叠 |
|
|
431
431
|
| `memoryQualityFilter` | `{enabled:true, archiveThreshold:30, degradeThreshold:60, minContentLength:10, exemptImportance:4}` | 记忆质量过滤(v0.4.6,默认开):写库前启发式打分 0-100,元记忆词汇/自指/过短/重复/近似重复扣分;≥60 正常存储,30-60 降权(注入排序按 importance×quality/100),<30 归档标记 `low_quality`(显式搜索仍可召回,永不自动注入;`exemptImportance` 豁免:importance ≥ 该值只降权不归档,#135) |
|
|
432
432
|
| `llmAudit` | `{enabled:true, retentionDays:90}` | LLM 消耗审计(v0.4.6,默认开):每次后台 LLM 调用(`autoDream` / `autoSummarize` / `sleep` / `entityExtract`,对应 `operation_type` 为 `dream_consolidate` / `dream_summarize` / `dream_narrative`、`summarize_compress`、`sleep_conflict` / `sleep_pattern`、`entity_extract`)写 `llm_audit_logs`(tokens/duration/status/source);失败记 error 不阻塞;只读 API `/api/dsh-mneme/semantic/llm-audit` + `/llm-audit/stats` |
|
|
433
|
-
| `scopeEnabled` | `false` | 作用域隔离总开关(v0.8.0,issue #17):memory_save 按会话身份写入 agent / workspace 标注(agentPreset / 工作区路径,registry 反查取不到回退 header.cwd,再取不到 NULL
|
|
433
|
+
| `scopeEnabled` | `false` | 作用域隔离总开关(v0.8.0,issue #17):memory_save 按会话身份写入 agent / workspace 标注(agentPreset / 工作区路径,registry 反查取不到回退 header.cwd,再取不到 NULL);去重键扩展含作用域三元——跨作用域同标题不再物理合并;检索与注入排序按当前会话作用域加权(命中 ×1.25、他 scope ×0.5 保留可见,issue #339 补齐注入通道)。也走 feature_flags 白名单(面板可启停) |
|
|
434
434
|
| `strictScope` | `false` | 作用域硬过滤(v0.8.0 引入,v0.8.1 起只认显式声明;依赖 `scopeEnabled`):检索 / 注入 / 列表 / 单取四路过滤,**显式声明**收窄到他者作用域的记忆完全不可见;载体自动标注只降权保留可见——真正的物理隔离请用 sensitivity。会话身份解析不到时 fail-closed 只挡显式行。关闭时全部为软隔离(降权保留可见)。也走 feature_flags 白名单 |
|
|
435
435
|
| `writeAdmission` | `{enabled:false, enforce:false}` | 写入准入(issue #254,默认关):把「这条该不该进库」前移到 LLM 之前。第 1 级是零 LLM 的确定性判据——空白 / 纯噪声 + 密钥 / PII(判据来源是 #164 A2,经 `sensitiveScan` 注入,本批只定义接口)。`enabled` 跑判据并落审计行(`llm_audit_logs` 的 `metadata.deny`),`enforce` 才真的拒(`store.save` 之前返回,`memory_save` 返回 `action:"denied"` + `reason`);只开 `enabled` = 仅告警、写入不拦。去重键命中不进第 1 级(归 write-update 放行),G1/G2 阈值只计量。也走 feature_flags 白名单 |
|
|
436
436
|
| `conflictFreezeEnabled` | `false` | 冲突冻结(v0.4.4):dream 发现矛盾对不自动裁决,冻结进冲突队列;状态页「冲突队列」支持并排对比与人工确认(保留 A / 保留 B / 仅标记已处理,v0.8.0) |
|
|
@@ -584,7 +584,7 @@ src/
|
|
|
584
584
|
├── api.js # HTTP 路由(Web 面板数据通道,含 /conflicts 冲突队列)
|
|
585
585
|
└── index.js # 插件接线
|
|
586
586
|
lib/ # src 的同步分发产物(npm run sync;发布前由 root prepack 的 check-sync.js 校验一致性;唯一手写例外 lib/client.js——Web 面板 bundle,sync 不覆盖)
|
|
587
|
-
test/ #
|
|
587
|
+
test/ # 1523 个 node:test 测试(审计与三轴线压测不变量;src↔lib 一致性由 scripts/check-sync.js 发布闸门校验)
|
|
588
588
|
scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步 · check-sync.js 发布闸门 · benchmark-recall.js / benchmark-embed.js / benchmark-rerank.js 基准 · sync-test-badge.mjs 测试徽章 · build-runtime-manifest.mjs 运行时清单
|
|
589
589
|
```
|
|
590
590
|
|
|
@@ -593,7 +593,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
|
|
|
593
593
|
```bash
|
|
594
594
|
cd dsh-mneme
|
|
595
595
|
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
|
|
596
|
-
npm test # 运行
|
|
596
|
+
npm test # 运行 1523 个测试
|
|
597
597
|
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
|
|
598
598
|
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
|
|
599
599
|
```
|
package/assets/icon.svg
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 16 16" fill="none" stroke="#4176e6" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
-
<path d="M4.43 12.31C2.19 10.17 2.27 6.81 4.18 4.84C6.10 2.88 9.06 2.99 10.74 4.68C12.43 6.37 12.28 8.92 10.82 10.33C9.35 11.74 7.22 11.55 6.08 10.32C4.95 9.08 5.18 7.35 6.18 6.50C6.57 6.17 7.03 5.99 7.48 5.97"/>
|
|
3
|
-
</svg>
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 16 16" fill="none" stroke="#4176e6" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
|
|
2
|
+
<path d="M4.43 12.31C2.19 10.17 2.27 6.81 4.18 4.84C6.10 2.88 9.06 2.99 10.74 4.68C12.43 6.37 12.28 8.92 10.82 10.33C9.35 11.74 7.22 11.55 6.08 10.32C4.95 9.08 5.18 7.35 6.18 6.50C6.57 6.17 7.03 5.99 7.48 5.97"/>
|
|
3
|
+
</svg>
|
package/lib/api.js
CHANGED
|
@@ -143,6 +143,8 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
143
143
|
"llmAudit.enabled": ["llmAudit", "enabled"],
|
|
144
144
|
"writeAdmission.enabled": ["writeAdmission", "enabled"],
|
|
145
145
|
"writeAdmission.enforce": ["writeAdmission", "enforce"]
|
|
146
|
+
// sensitiveScanEnabled(#164 A2)是顶层扁平键,不需要进这张表——走 configFlagValue
|
|
147
|
+
// 的默认分支即可。
|
|
146
148
|
};
|
|
147
149
|
function configFlagValue(key) {
|
|
148
150
|
const path = NESTED_FLAG_PATHS[key];
|
package/lib/client.js
CHANGED
|
@@ -493,6 +493,11 @@ window.__ModuleLoader__.load({
|
|
|
493
493
|
"memory.features.writeAdmission.enabled.hint": "写入前判空白/噪声,命中留审计(不拦)",
|
|
494
494
|
"memory.features.writeAdmission.enforce": "写入准入拦截",
|
|
495
495
|
"memory.features.writeAdmission.enforce.hint": "真拦下命中的写入;关闭时只留审计不拦",
|
|
496
|
+
// #164 A2:密钥/PII 判据自己的闸。文案要写清它管的是哪一批判据(与上面的
|
|
497
|
+
// 空白/噪声分开)、它跑在「写入准入判定」里面(那一项关着就不跑),以及
|
|
498
|
+
// 「开着也不拦」——拦是上面那项的事。
|
|
499
|
+
"memory.features.sensitiveScanEnabled": "密钥/PII 扫描",
|
|
500
|
+
"memory.features.sensitiveScanEnabled.hint": "写入前扫密钥与个人信息;需先打开写入准入判定才会执行,命中只留审计,要拦再打开写入准入拦截",
|
|
496
501
|
"memory.features.bm25SearchEnabled": "BM25 关键词检索",
|
|
497
502
|
"memory.features.bm25SearchEnabled.hint": "传统关键词打分检索,与向量召回互补",
|
|
498
503
|
"memory.features.conflictFreezeEnabled": "冲突冻结",
|
|
@@ -925,6 +930,12 @@ window.__ModuleLoader__.load({
|
|
|
925
930
|
"memory.features.writeAdmission.enabled.hint": "Flag blank / noise writes before storing, with an audit row (never blocks)",
|
|
926
931
|
"memory.features.writeAdmission.enforce": "Write admission enforcement",
|
|
927
932
|
"memory.features.writeAdmission.enforce.hint": "Actually reject flagged writes; off keeps the audit row only",
|
|
933
|
+
// #164 A2: its own gate for the secret / PII rules (separate from the
|
|
934
|
+
// blank/noise pair above), and it runs inside the write admission checks
|
|
935
|
+
// above, so with those off it never runs. Hits are audited only;
|
|
936
|
+
// enforcement stays with the key above.
|
|
937
|
+
"memory.features.sensitiveScanEnabled": "Secret / PII scan",
|
|
938
|
+
"memory.features.sensitiveScanEnabled.hint": "Scan writes for credentials and personal data; needs write admission checks to run. Hits are audited only; enforcement needs the enforcement toggle too",
|
|
928
939
|
"memory.features.bm25SearchEnabled": "BM25 keyword search",
|
|
929
940
|
"memory.features.bm25SearchEnabled.hint": "Classic keyword scoring, complementary to vector recall",
|
|
930
941
|
"memory.features.conflictFreezeEnabled": "Conflict freezing",
|
|
@@ -2122,7 +2133,7 @@ window.__ModuleLoader__.load({
|
|
|
2122
2133
|
// test/inject-parent-gate.test.js 钉住。
|
|
2123
2134
|
const FEATURE_CHILDREN = { autoInject: ["injectGuidanceEnabled", "continuityRescueEnabled"] };
|
|
2124
2135
|
const FEATURE_GROUPS = [
|
|
2125
|
-
{ key: "group.core", items: ["autoInject", "autoSummarize", "hotMemoryEnabled", "injectTimePrefix", "memoryQualityFilter.enabled", "llmAudit.enabled", "writeAdmission.enabled", "writeAdmission.enforce"] },
|
|
2136
|
+
{ key: "group.core", items: ["autoInject", "autoSummarize", "hotMemoryEnabled", "injectTimePrefix", "memoryQualityFilter.enabled", "llmAudit.enabled", "writeAdmission.enabled", "writeAdmission.enforce", "sensitiveScanEnabled"] },
|
|
2126
2137
|
{ key: "group.enhance", items: ["entityExtractionEnabled", "codingRetrospect", "rerankEnabled", "resilientModelDownload", "searchSemanticDedup", "bm25SearchEnabled", "heatEnabled", "documentMemoryEnabled"] },
|
|
2127
2138
|
{ key: "group.dream", items: ["autoDream", "sleepModeEnabled"] },
|
|
2128
2139
|
// v0.8.0 A4(issue #17):作用域隔离组——标注总开关 + 严格硬过滤。
|
package/lib/config.js
CHANGED
|
@@ -281,6 +281,14 @@ export const Config = z.object({
|
|
|
281
281
|
// 显式开启后放宽跨类型合并检查(类型边界由用户自行承担);配合
|
|
282
282
|
// dreamSkipInvalid 理解:关闭 skipInvalid 时跨类型 merge 直接整单拒绝。
|
|
283
283
|
allowCrossTypeMerge: z.boolean().default(false),
|
|
284
|
+
// issue #339 / E8 考卷:merge 护栏(opt-in,默认关 = 现状)。开启后合并对象
|
|
285
|
+
// 命中长保留类型(preference/pattern/rejected_solution/constraint/pitfall,
|
|
286
|
+
// 与 archive 护栏同一张表)的 merge 决策整条跳过——E8 实测巩固损耗里
|
|
287
|
+
// 10/26 条被丢约束已归位 guarded 类型仍被 merge 吃掉,而 archive 护栏只挡
|
|
288
|
+
// archive 不挡 merge,「更精炼的摘要」恰是约束失真的主通道。skipInvalid
|
|
289
|
+
// (默认开)时被跳条目进 dream_runs.skipped、run 记 degraded;关闭
|
|
290
|
+
// skipInvalid 时整单拒绝。与 allowCrossTypeMerge 同用时长 guard 先判。
|
|
291
|
+
dreamMergeGuard: z.boolean().default(false),
|
|
284
292
|
// Rule version for dream adjudication: when this bumps, older dream_runs
|
|
285
293
|
// degrade to historical evidence (their receipts no longer drive live
|
|
286
294
|
// decisions). Default 0 = no versioning in use yet.
|
|
@@ -463,6 +471,35 @@ export const Config = z.object({
|
|
|
463
471
|
// 检索融合池(与 BM25 同级的确认/回填信号)。默认关=检索行为与 #219 前
|
|
464
472
|
// 逐字节一致;走 feature_flags 面板可启停,lightMode 强制关闭。
|
|
465
473
|
entityRecallEnabled: z.boolean().default(false),
|
|
474
|
+
// 边权重演化(issue #24 块2):touch 门控是否把被寻回路径的边权重抬升。
|
|
475
|
+
// 默认关=边初值纯由建边来源决定(store.RELATION_SOURCE_DEFAULTS),演化
|
|
476
|
+
// 不生效;开=被激活的边经 bumpRelationWeight 上涨(只加不减、封顶 1.0)。
|
|
477
|
+
graphWeightEnabled: z.boolean().default(false),
|
|
478
|
+
// 单次有效触达的抬升幅度。
|
|
479
|
+
graphWeightDelta: z.number().min(0).max(1).default(0.1),
|
|
480
|
+
// 被动确认(issue #24 块4):heat 触达(记忆被召回/注入=被用)即对该记忆
|
|
481
|
+
// 挂联的关系边做被动确认——bumpRelationWeight 抬一格。默认关=零行为变化;
|
|
482
|
+
// 开=正常触达演化边权,「关系是用出来的」闭环落地(异常路径复核留后续块)。
|
|
483
|
+
graphPassiveConfirm: z.boolean().default(false),
|
|
484
|
+
|
|
485
|
+
// --- 图谱锚定层(issue #24 · 块1 Activation Anchoring)-------------------
|
|
486
|
+
// entityRecallEnabled 之上再开一层:以查询命中的实体为种子,沿实体关系表
|
|
487
|
+
// 级联扩散(默认深度 2),把「邻居实体挂联的记忆」也带进融合池。种子集
|
|
488
|
+
// 上限 graphSeedCap、级联深度 graphCascadeDepth 一并下发到 anchorSeeds /
|
|
489
|
+
// cascadeDepths(见 src/graph/anchoring.js)。默认关=行为与 #219 单跳轴
|
|
490
|
+
// 逐字节一致;走 feature_flags 面板可启停,lightMode 强制关闭。
|
|
491
|
+
graphAnchoringEnabled: z.boolean().default(false),
|
|
492
|
+
graphSeedCap: z.natural().min(1).max(30).default(12),
|
|
493
|
+
graphCascadeDepth: z.natural().min(1).max(3).default(2),
|
|
494
|
+
|
|
495
|
+
// 关联提示防幻觉(issue #24 块3):图召回的候选(查询命中实体的挂联记忆)
|
|
496
|
+
// 进注入时默认只参与排序、不改变注入块构成(graphInjectHint=false 保守档);
|
|
497
|
+
// 开启后注入文本里以固定句式「[检索线索] …」标出,引导语说明这是链路信息
|
|
498
|
+
// 非事实断言,防 LLM 单次消费把桥接召回当有依据的事实回头扩散。
|
|
499
|
+
graphInjectHint: z.boolean().default(false),
|
|
500
|
+
// 图召回候选在注入块里的独立预算(条数上限)。与 pin/document 预算同构:
|
|
501
|
+
// 不占 maxInjectedItems 名额、只约束「桥接线索」这类低信任信息的常驻占比。
|
|
502
|
+
graphInjectBudget: z.natural().min(0).max(5).default(1),
|
|
466
503
|
|
|
467
504
|
// --- sleep mode: idle-triggered deep maintenance (v0.4.0) ---------------
|
|
468
505
|
// Opt-in, off by default. Unlike autoDream (threshold-triggered, lightweight)
|
|
@@ -614,6 +651,25 @@ export const Config = z.object({
|
|
|
614
651
|
enforce: z.boolean().default(false)
|
|
615
652
|
}).default({}),
|
|
616
653
|
|
|
654
|
+
// --- #164 A2: secret / PII scan at the write boundary ----------------------
|
|
655
|
+
// 写入边界的密钥 / PII 判据(src/sensitive-scan.js)自身的闸。与 writeAdmission
|
|
656
|
+
// 的 enabled 分开是有意的:那一个管 #254 第 1 级的空白 / 噪声判据,本键管 A2 这
|
|
657
|
+
// 一类判据,两批的误杀面差一个量级(空白 / 噪声没有解释空间,邮箱 / 手机号有),
|
|
658
|
+
// 绑在同一个开关上就没法单独观察 A2 的命中分布。
|
|
659
|
+
//
|
|
660
|
+
// 分层必须写清:判据的唯一调用点在 #254 第 1 级的闸门里(write-admission.js 的
|
|
661
|
+
// firstLevelHit),闸门不走第 1 级就没人来调这个扫描器。所以本键是「闸门内这一批
|
|
662
|
+
// 判据参不参与」,不是一条能独立跑的链路——单开本键就是零行为变化:
|
|
663
|
+
// writeAdmission.enabled 关 → 第 1 级整个不跑,本键开也没用
|
|
664
|
+
// enabled 开 + 本键关 → 只跑空白 / 噪声那一批
|
|
665
|
+
// enabled 开 + 本键开 + enforce 关 → 命中留审计,写入照常(观察档)
|
|
666
|
+
// enabled 开 + 本键开 + enforce 开 → 命中即拒绝
|
|
667
|
+
// 默认关 = 只计量那一阶段的行为逐字节保留(#332 合并时 sensitiveScan 就是 null)。
|
|
668
|
+
// #164 的「默认仅告警、拦截 opt-in」落在 enforce 上:命中落一条审计
|
|
669
|
+
// (metadata.deny.kind)但照常写入,真要拦得 enabled 与 enforce 同时开。
|
|
670
|
+
// 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
|
|
671
|
+
sensitiveScanEnabled: z.boolean().default(false),
|
|
672
|
+
|
|
617
673
|
// --- recall evaluation: test-result storage (v0.4.6, 方案 B) --------------
|
|
618
674
|
// Separate retrieval evaluation snapshots from the production recall audit.
|
|
619
675
|
// When false (default) evaluateRetrieval still computes precision/recall/mrr
|
|
@@ -685,7 +741,8 @@ export const Config = z.object({
|
|
|
685
741
|
// agent_scope, workspace_scope, sensitivity)。检索侧加权/过滤在 A2/A3 落地。
|
|
686
742
|
// 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
|
|
687
743
|
scopeEnabled: z.boolean().default(false),
|
|
688
|
-
// strictScope(A3):硬过滤模式。关闭=A2 软隔离(他 scope
|
|
744
|
+
// strictScope(A3):硬过滤模式。关闭=A2 软隔离(他 scope 降权保留可见,
|
|
745
|
+
// 检索与注入排序同权重,issue #339 补齐注入通道);
|
|
689
746
|
// 开启后检索/注入/list/get 按 issue #17 四象限可见性公式硬过滤——带他 scope
|
|
690
747
|
// 的记忆完全不可见,未标注(NULL)恒可见;当前会话某维度解析不到时该维度
|
|
691
748
|
// 带标注的记忆一律不可见(fail-closed:身份不明只见全局)。依赖 scopeEnabled
|
|
@@ -712,6 +769,14 @@ const LIGHT_MODE_OFF = [
|
|
|
712
769
|
"bm25SearchEnabled",
|
|
713
770
|
// 轻量模式不开图召回轴(#219,依赖实体抽取产出;抽取本身已被关掉)。
|
|
714
771
|
"entityRecallEnabled",
|
|
772
|
+
// 轻量模式不开图谱锚定层级联(#24 块1,依赖实体抽取 + 关系表,抽取已关则无边可扩)。
|
|
773
|
+
"graphAnchoringEnabled",
|
|
774
|
+
// 轻量模式不开边权重演化(#24 块2,依赖实体抽取产出;抽取关则无 v08 边可抬升)。
|
|
775
|
+
"graphWeightEnabled",
|
|
776
|
+
// 轻量模式不开关联提示标注(#24 块3,常驻注入文本多一段提示成本)。
|
|
777
|
+
"graphInjectHint",
|
|
778
|
+
// 轻量模式不开被动确认(#24 块4,依赖实体抽取产出;抽取关则无 v08 边可抬升)。
|
|
779
|
+
"graphPassiveConfirm",
|
|
715
780
|
// 轻量模式不开叙述条(额外 LLM 调用;#164 对齐,opt-in)。
|
|
716
781
|
"dreamNarrativeEnabled",
|
|
717
782
|
// 轻量模式不开 document 指针行(#230,opt-in:注册/注入/检索增强全随闸)。
|
package/lib/dream/decisions.js
CHANGED
|
@@ -210,6 +210,22 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
210
210
|
if (mergeTypes.size > 1 && options.allowCrossTypeMerge !== true) {
|
|
211
211
|
local.push(`${at}: merge ids span multiple types (${[...mergeTypes].join(", ")})`);
|
|
212
212
|
}
|
|
213
|
+
// issue #339 / E8 考卷:merge 护栏(opt-in,dreamMergeGuard)。巩固损耗
|
|
214
|
+
// 实测里 10/26 条被丢约束已归位 guarded 类型仍被 merge 吃掉——archive
|
|
215
|
+
// 护栏(上方)只挡 archive 不挡 merge,而 merge 的「更精炼摘要」恰是
|
|
216
|
+
// 约束失真的主通道。开启后被合并对象命中 ARCHIVE_GUARDED_TYPES 的
|
|
217
|
+
// merge 决策整条跳过(与 archive 护栏同款通道:skipInvalid 时 skipped
|
|
218
|
+
// 明细、严格模式整单拒绝)。
|
|
219
|
+
if (local.length === 0 && options.mergeGuard === true) {
|
|
220
|
+
const guardedTypes = new Set();
|
|
221
|
+
for (const id of d.ids) {
|
|
222
|
+
const mem = snapshot.get(id);
|
|
223
|
+
if (mem && ARCHIVE_GUARDED_TYPES.has(mem.type)) guardedTypes.add(mem.type);
|
|
224
|
+
}
|
|
225
|
+
if (guardedTypes.size > 0) {
|
|
226
|
+
local.push(`${at}: merge of long-retention type(s) ${[...guardedTypes].join(", ")} is blocked by dreamMergeGuard`);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
213
229
|
}
|
|
214
230
|
}
|
|
215
231
|
if (local.length > 0) {
|
package/lib/dream/sleep.js
CHANGED
|
@@ -373,6 +373,9 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
373
373
|
minAgeHours: config.reflectionUpdateMinAgeHours,
|
|
374
374
|
skipInvalid: config.dreamSkipInvalid !== false,
|
|
375
375
|
allowCrossTypeMerge: config.allowCrossTypeMerge === true,
|
|
376
|
+
// issue #339 / E8:guarded 类型 merge 护栏(opt-in,dreamMergeGuard),
|
|
377
|
+
// 与 dream 主链路同一判据。
|
|
378
|
+
mergeGuard: config.dreamMergeGuard === true,
|
|
376
379
|
// Issue #126 review(Copilot):默认档必须真的只放行旧动作集——只换 prompt 挡不住
|
|
377
380
|
// 模型自发输出 supersede / differentiate,那样"opt-in 零行为变化"就不成立。
|
|
378
381
|
allowedActions: actionSet === "full" ? null : LEGACY_ACTIONS
|
package/lib/dream.js
CHANGED
|
@@ -52,6 +52,26 @@ function totalChars(memories) {
|
|
|
52
52
|
return memories.reduce((sum, m) => sum + (m.title?.length ?? 0) + (m.content?.length ?? 0), 0);
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
+
// 阈值口径的唯一实现:活跃记忆(未归档、非 summary/document)的条数与正文字符数。
|
|
56
|
+
// 触发判定(shouldTrigger)与「成功轮把基线落库」(runDream 的 finish)都走这里——
|
|
57
|
+
// 两处若各写一份过滤条件,重启播种读到的基线就会和运行期判定口径漂移(一个把
|
|
58
|
+
// document 算进去、一个不算),阈值随之失真,且这种漂移只会在重启后才显形。
|
|
59
|
+
function activeStoreSize(service) {
|
|
60
|
+
const memories = service.all().filter((m) => !m.archived && m.type !== "summary" && m.type !== "document");
|
|
61
|
+
return { count: memories.length, chars: totalChars(memories) };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// 基线的唯一合法形状:两个非负整数;其余(缺字段、负数、非整数、整个对象缺失)
|
|
65
|
+
// 一律视为「没有基线」→ 调用方退回 {0,0}。播种值来自持久化列,可能是老库的 NULL
|
|
66
|
+
// 或手工改坏的库,判定必须比调用点更严格,而不是相信数据库里存的一定合法。
|
|
67
|
+
function toBaseline(raw) {
|
|
68
|
+
if (!raw || typeof raw !== "object") return null;
|
|
69
|
+
const count = Number.isInteger(raw.count) && raw.count >= 0 ? raw.count : null;
|
|
70
|
+
const chars = Number.isInteger(raw.chars) && raw.chars >= 0 ? raw.chars : null;
|
|
71
|
+
if (count === null || chars === null) return null;
|
|
72
|
+
return { count, chars };
|
|
73
|
+
}
|
|
74
|
+
|
|
55
75
|
// ---------------------------------------------------------------- audit
|
|
56
76
|
|
|
57
77
|
/**
|
|
@@ -760,7 +780,7 @@ export async function maintainIndexAfterDream(decisions, service, semantic) {
|
|
|
760
780
|
// 用户配得比这更大的 dreamMinIntervalMinutes 基数压小(见 effectiveMinIntervalMs)。
|
|
761
781
|
const FAILURE_BACKOFF_CAP_MS = 30 * 60 * 1000;
|
|
762
782
|
|
|
763
|
-
export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChars = 5000, delayMs = 2000, minIntervalMs = 0, failureBackoff = false, logger, semantic = null, lastRunAtSeed = 0, peakHours = "", peakMaxDeferMinutes = 120, auditPeakSkip = null, now = () => Date.now(), setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout }) {
|
|
783
|
+
export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChars = 5000, delayMs = 2000, minIntervalMs = 0, failureBackoff = false, logger, semantic = null, lastRunAtSeed = 0, baselineSeed = null, peakHours = "", peakMaxDeferMinutes = 120, auditPeakSkip = null, now = () => Date.now(), setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout }) {
|
|
764
784
|
let pendingTimer = null;
|
|
765
785
|
// Issue #239(第 4 项)镜像到巩固:高峰顺延定时器。与 pendingTimer 分开——两者
|
|
766
786
|
// 语义不同(一个是「马上要跑」,一个是「等出高峰再跑」),合成一个变量会让
|
|
@@ -768,7 +788,13 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
768
788
|
let deferTimer = null;
|
|
769
789
|
let running = false;
|
|
770
790
|
let disposed = false;
|
|
771
|
-
|
|
791
|
+
// Issue #89(基线半边):阈值基线跨重启持久化。baselineSeed 由调用方从
|
|
792
|
+
// dream_runs 读上次成功轮结束时的库规模(store.lastDreamBaseline)——它只活在
|
|
793
|
+
// 闭包内存里时进程重启即归零,而库里记忆远多于阈值,重启后第一条写入就满足
|
|
794
|
+
// overBase、绕过阈值开跑整轮(#291 只补了 lastRunAt 那半边)。缺种子(新库、
|
|
795
|
+
// 升级后还没跑过成功轮)或种子非法时退回 {0,0}:不假装知道基线,行为与升级前
|
|
796
|
+
// 逐字节一致——修的是「知道却不认」,不是凭空造一个基线。
|
|
797
|
+
let baseline = toBaseline(baselineSeed) ?? { count: 0, chars: 0 };
|
|
772
798
|
let inFlight = null;
|
|
773
799
|
// Issue #89(请求 2):上一次实际开跑时刻。失败/degraded 的 run 也占用
|
|
774
800
|
// 最小间隔——节流的目的正是防止失败调用连发;间隔内的触发静默跳过。
|
|
@@ -792,9 +818,7 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
792
818
|
}
|
|
793
819
|
|
|
794
820
|
function shouldTrigger(service) {
|
|
795
|
-
const
|
|
796
|
-
const count = memories.length;
|
|
797
|
-
const chars = totalChars(memories);
|
|
821
|
+
const { count, chars } = activeStoreSize(service);
|
|
798
822
|
const overBase = count >= baseline.count + thresholdCount || chars >= baseline.chars + thresholdChars;
|
|
799
823
|
const overAbs = count >= thresholdCount || chars >= thresholdChars;
|
|
800
824
|
return { trigger: overAbs && overBase, count, chars };
|
|
@@ -881,7 +905,10 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
881
905
|
if (result && result.ok) {
|
|
882
906
|
consecutiveFailures = 0; // Issue #292:成功清零,下次触发回到基数间隔
|
|
883
907
|
try {
|
|
884
|
-
|
|
908
|
+
// Issue #89(基线半边):run 自报的基线(与审计行同一份数字)优先——重启
|
|
909
|
+
// 播种读的就是它,两条路径共用唯一口径;no-op 桩不回这个字段时退回现场
|
|
910
|
+
// 计算,最小测试替身的行为逐字节不变。
|
|
911
|
+
baseline = toBaseline(result.baseline) ?? shouldTrigger(service);
|
|
885
912
|
} catch (error) {
|
|
886
913
|
// Store closed mid-flight: keep the last known baseline.
|
|
887
914
|
logger?.warn?.(`dsh-mneme dream: baseline refresh failed: ${String(error)}`);
|
|
@@ -986,6 +1013,19 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
986
1013
|
const applied = result.applied ?? 0;
|
|
987
1014
|
const summaryStored = result.summary ?? false;
|
|
988
1015
|
const receipt = buildReceipt({ runId, status, snapshotHash, inputCount: snapshot.size, applied, summaryStored });
|
|
1016
|
+
// Issue #89(基线半边):成功轮把「本轮结束后的库规模」落进审计行 = 阈值基线的
|
|
1017
|
+
// 持久化副本,重启后由 store.lastDreamBaseline 播种。判定条件必须与调度器刷新
|
|
1018
|
+
// 基线的条件严格一致(result.ok)——非 ok 轮不推进基线是既有语义,若这里也落值,
|
|
1019
|
+
// 重启后播种就会拿到一个「其实没推进过」的假基线,阈值反而被抬高。
|
|
1020
|
+
// 取不到(库已关闭等)就落 NULL:宁可不持久化,也不写一个错的基线。
|
|
1021
|
+
let baselineSnapshot = null;
|
|
1022
|
+
if (result.ok) {
|
|
1023
|
+
try {
|
|
1024
|
+
baselineSnapshot = activeStoreSize(service);
|
|
1025
|
+
} catch {
|
|
1026
|
+
baselineSnapshot = null;
|
|
1027
|
+
}
|
|
1028
|
+
}
|
|
989
1029
|
try {
|
|
990
1030
|
service.saveDreamRun({
|
|
991
1031
|
id: runId,
|
|
@@ -1023,12 +1063,18 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1023
1063
|
// 而非内联进 decisions——degraded 的 decisions 是合法子集,下游按「决策
|
|
1024
1064
|
// 数组」消费,内联标记会污染其它读者(失败轮的 _validationFailed 是整单
|
|
1025
1065
|
// 拒绝哨兵,语义与「部分应用」不同,也不复用)。
|
|
1026
|
-
skipped: result.skipped
|
|
1066
|
+
skipped: result.skipped,
|
|
1067
|
+
// Issue #89(基线半边):成功的 run 才带这两个值(见上方 baselineSnapshot
|
|
1068
|
+
// 的注释)。两列要么同时有值、要么同时为 NULL,读侧以「同时有值」认账。
|
|
1069
|
+
store_count: baselineSnapshot?.count,
|
|
1070
|
+
store_chars: baselineSnapshot?.chars
|
|
1027
1071
|
});
|
|
1028
1072
|
} catch (error) {
|
|
1029
1073
|
logger?.warn?.(`dsh-mneme dream: failed to record audit run: ${String(error)}`);
|
|
1030
1074
|
}
|
|
1031
|
-
|
|
1075
|
+
// Issue #89(基线半边):把「与审计行同一份数字」的基线回给调度器,两条路径
|
|
1076
|
+
// (进程内刷新 / 跨重启播种)因此共享唯一口径;调度器拿不到时会退出现场计算。
|
|
1077
|
+
return { ...result, runId, receipt, snapshotHash, baseline: baselineSnapshot ?? undefined };
|
|
1032
1078
|
};
|
|
1033
1079
|
if (!route) {
|
|
1034
1080
|
logger?.warn?.("dsh-mneme dream: no llm route available");
|
|
@@ -1135,7 +1181,9 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1135
1181
|
// Issue #89:v0.6.9(Issue #26)的宽容路径在 v0.7.11 重写中丢失——单条
|
|
1136
1182
|
// 非法决策重新只跳过该条、合法子集照常应用(run 记为 degraded)。
|
|
1137
1183
|
skipInvalid: config.dreamSkipInvalid !== false,
|
|
1138
|
-
allowCrossTypeMerge: config.allowCrossTypeMerge === true
|
|
1184
|
+
allowCrossTypeMerge: config.allowCrossTypeMerge === true,
|
|
1185
|
+
// issue #339 / E8:guarded 类型 merge 护栏(opt-in,dreamMergeGuard)。
|
|
1186
|
+
mergeGuard: config.dreamMergeGuard === true
|
|
1139
1187
|
});
|
|
1140
1188
|
// Issue #135:模型把 UUID 缩写成前缀时,唯一前缀已在校验前被解析回完整 id。
|
|
1141
1189
|
// 解析量是模型输出质量的一个直接信号(>0 意味着模型在缩写 id),记一条 info
|
|
@@ -226,7 +226,10 @@ export async function extractEntities(memory, { store, config, callLLM, logger }
|
|
|
226
226
|
to_entity: toId,
|
|
227
227
|
relation_type: rel.type,
|
|
228
228
|
memory_id: memory.id,
|
|
229
|
-
metadata: { model: options.model || "default" }
|
|
229
|
+
metadata: { model: options.model || "default" },
|
|
230
|
+
// issue #24 块2:LLM 抽取自动建边 → llm 来源(初值 0.4),区别于
|
|
231
|
+
// 人工/确认的满权边。来源进 metadata 之上的独立列,演化与审计可辨。
|
|
232
|
+
source: "llm"
|
|
230
233
|
});
|
|
231
234
|
savedRelations.push(saved);
|
|
232
235
|
} catch (err) {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
// dsh-mneme/src/graph/anchoring.js
|
|
2
|
+
// 图谱锚定层(issue #24 · 块1 Activation Anchoring)。
|
|
3
|
+
import assert from "node:assert/strict";
|
|
4
|
+
// 纯函数模块:把「查询命中实体的多路激活信号」归一化、合并成种子集、再沿
|
|
5
|
+
// entity_relations 级联扩散。零数据库依赖——store 侧查询由调用方注入,
|
|
6
|
+
// 本模块只做信号处理,便于单测与与其他召回通路(BM25/向量)同等对待。
|
|
7
|
+
//
|
|
8
|
+
// 设计输入来自 #24 合议 + heptaspirit serendipity-engine 实战(#24 评论):
|
|
9
|
+
// - 归一化二分法:连续分(cos/PPR)走 min-max;匹配通路(精确/tag/别名)
|
|
10
|
+
// 走离散级别,不用 min-max——布尔通路用 min-max 会把「中 1 个」和「中
|
|
11
|
+
// 20 个」都拉平到 1.0,级别信息被压平。
|
|
12
|
+
// - 级联扩散加「跳数配额」:λ/θ 只管可及性,可见性由配额决定——直接按 hop
|
|
13
|
+
// 深度打 0.5/0.3/0.2 权重,配桶内 round-robin,防深跳被 1-hop 完全压住。
|
|
14
|
+
// - graphSeedCap:防「多锚点命中 + 模糊通路」把种子集撑爆成整库。
|
|
15
|
+
// - 排序稳定:并列按实体 ID 破序(同输入两次结果不同会让 A/B 调参白做)。
|
|
16
|
+
|
|
17
|
+
/** 匹配通路的离散级别(heptaspirit 建议:布尔/计数通路不用 min-max)。 */
|
|
18
|
+
export const DISCRETE_LEVELS = Object.freeze({
|
|
19
|
+
exact: 1.0, // 名称精确含于查询(布尔 1.0)
|
|
20
|
+
title: 0.8, // 实体名匹配记忆标题(tag/别名层)
|
|
21
|
+
alias: 0.6,
|
|
22
|
+
tag: 0.4, // tag 共现
|
|
23
|
+
substring: 0.2, // 子串命中(最弱,纯计数不级联)
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
/** 级联每跳的权重(跳数配额:1:2:3-hop = 0.5/0.3/0.2)。 */
|
|
27
|
+
export const HOP_QUOTA = Object.freeze({ 1: 0.5, 2: 0.3, 3: 0.2 });
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* 连续分 min-max 归一化(向量 cos 相似度、PPR 激活分等)。
|
|
31
|
+
* 输入 [] 或全同值时返回原样(无尺度可压平,防除零)。
|
|
32
|
+
*/
|
|
33
|
+
export function minMax(scores) {
|
|
34
|
+
const arr = Array.isArray(scores) ? scores : [];
|
|
35
|
+
if (arr.length === 0) return [];
|
|
36
|
+
let min = Infinity;
|
|
37
|
+
let max = -Infinity;
|
|
38
|
+
for (const v of arr) {
|
|
39
|
+
if (v < min) min = v;
|
|
40
|
+
if (v > max) max = v;
|
|
41
|
+
}
|
|
42
|
+
if (max - min < 1e-12) return arr.map(() => 1); // 平局视为满权
|
|
43
|
+
return arr.map((v) => (v - min) / (max - min));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* 把一条检测通路的结果归一化成 0..1 的打分表。
|
|
48
|
+
* path 是离散通路名(exact/tag/...)→ 查 DISCRETE_LEVELS;是连续通路
|
|
49
|
+
* (continuous=true)→ min-max。返回 { byId: Map<id, score>, order: [id...] }。
|
|
50
|
+
*/
|
|
51
|
+
export function normalizePath(hits, { path = "exact", continuous = false } = {}) {
|
|
52
|
+
const ids = Array.isArray(hits) ? hits : [];
|
|
53
|
+
const byId = new Map();
|
|
54
|
+
if (continuous) {
|
|
55
|
+
const raw = ids.map((h) => (typeof h === "number" ? h : h?.score ?? 0));
|
|
56
|
+
const norm = minMax(raw);
|
|
57
|
+
ids.forEach((h, i) => {
|
|
58
|
+
const id = typeof h === "object" ? h.id : h;
|
|
59
|
+
byId.set(id, norm[i]);
|
|
60
|
+
});
|
|
61
|
+
} else {
|
|
62
|
+
const level = DISCRETE_LEVELS[path] ?? DISCRETE_LEVELS.substring;
|
|
63
|
+
for (const id of ids) byId.set(id, level);
|
|
64
|
+
}
|
|
65
|
+
return { byId, order: [...byId.keys()] };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* 合并多路激活成种子集:每路先归一化(调用方负责通路分类),再累加。
|
|
70
|
+
* seedsCap 上限(graphSeedCap,默认 12);封顶前按总分排序取前 N——保证
|
|
71
|
+
* 扩散的是活跃子图而非冷启动点爆整库。
|
|
72
|
+
* 返回 { id, name, score, paths: Set } 列表,按 score 降序、name 升序(稳定)。
|
|
73
|
+
*/
|
|
74
|
+
export function anchorSeeds({ paths = [], cap = 12 } = {}) {
|
|
75
|
+
const score = new Map();
|
|
76
|
+
const nameOf = new Map();
|
|
77
|
+
const pathSet = new Map();
|
|
78
|
+
for (const p of paths) {
|
|
79
|
+
for (const [id, s] of p.byId ?? []) {
|
|
80
|
+
score.set(id, (score.get(id) ?? 0) + s);
|
|
81
|
+
if (!pathSet.has(id)) pathSet.set(id, new Set());
|
|
82
|
+
pathSet.get(id).add(p.path ?? "?");
|
|
83
|
+
if (p.nameOf?.has?.(id)) nameOf.set(id, p.nameOf.get(id));
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
let seeds = [...score.entries()].map(([id, s]) => ({
|
|
87
|
+
id,
|
|
88
|
+
name: nameOf.get(id) ?? id,
|
|
89
|
+
score: s,
|
|
90
|
+
paths: pathSet.get(id) ?? new Set()
|
|
91
|
+
}));
|
|
92
|
+
seeds.sort((a, b) => b.score - a.score || (a.name < b.name ? -1 : a.name > b.name ? 1 : 0));
|
|
93
|
+
if (cap > 0 && seeds.length > cap) seeds = seeds.slice(0, cap);
|
|
94
|
+
return seeds;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 级联扩散:从种子实体出发沿 adjacency 走 BFS,为每个可达实体贴最短深度。
|
|
99
|
+
* depth 上界默认 2([1,3] 配置范围)。返回 Map<entityId, minDepth>——
|
|
100
|
+
* 调用方据此取 HOP_QUOTA[minDepth] 权重 + 桶内 round-robin(见 pickByHopQuota)。
|
|
101
|
+
* adjacency 由调用方提供(store.getEntityNeighbors(id) 或全量邻接矩阵),
|
|
102
|
+
* 本模块不感知存储。
|
|
103
|
+
*/
|
|
104
|
+
export function cascadeDepths({ seeds, adjacencyOf, maxDepth = 2 } = {}) {
|
|
105
|
+
const depths = new Map();
|
|
106
|
+
const idOfSeeds = (seeds ?? []).map((s) => (typeof s === "object" ? s.id : s));
|
|
107
|
+
const queue = [];
|
|
108
|
+
for (const id of idOfSeeds) {
|
|
109
|
+
if (!depths.has(id)) {
|
|
110
|
+
depths.set(id, 0);
|
|
111
|
+
queue.push([id, 0]);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
for (let i = 0; i < queue.length; i++) {
|
|
115
|
+
const [id, d] = queue[i];
|
|
116
|
+
if (d >= maxDepth) continue;
|
|
117
|
+
let neighbors = [];
|
|
118
|
+
try {
|
|
119
|
+
neighbors = typeof adjacencyOf === "function" ? adjacencyOf(id) : (adjacencyOf?.get?.(id) ?? []);
|
|
120
|
+
} catch { /* single neighbor fetch failure: keep spreading from the rest */ }
|
|
121
|
+
for (const nb of neighbors) {
|
|
122
|
+
const nid = typeof nb === "object" ? nb.id : nb;
|
|
123
|
+
if (!nid || depths.has(nid)) continue;
|
|
124
|
+
depths.set(nid, d + 1);
|
|
125
|
+
queue.push([nid, d + 1]);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return depths;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* 跳数配额选取:候选实体按深度分层,层内按(分,ID)稳定排序后取额度。
|
|
133
|
+
* quota 形如 { 1: 0.5, 2: 0.3, 3: 0.2 }——每层最多取 ceil(depthCap * quota[d]) 个,
|
|
134
|
+
* 桶内按 score 降序(并列按 ID)。返回 { ids, scoreOf, depthOf }。
|
|
135
|
+
* 这一步把「级联可及性」与「最终可见性」解耦(heptaspirit:λ/θ 只管前者)。
|
|
136
|
+
*/
|
|
137
|
+
export function pickByHopQuota({
|
|
138
|
+
depths = new Map(),
|
|
139
|
+
scoreOf = () => 0,
|
|
140
|
+
maxDepth = 2,
|
|
141
|
+
depthCap = 12
|
|
142
|
+
} = {}) {
|
|
143
|
+
const buckets = new Map(); // depth -> [{id, score}]
|
|
144
|
+
for (const [id, d] of depths) {
|
|
145
|
+
if (d === 0 || d > maxDepth) continue;
|
|
146
|
+
const key = Math.min(d, 3);
|
|
147
|
+
if (!buckets.has(key)) buckets.set(key, []);
|
|
148
|
+
buckets.get(key).push({ id, score: scoreOf(id) ?? 0 });
|
|
149
|
+
}
|
|
150
|
+
const picked = [];
|
|
151
|
+
for (const [d, items] of buckets) {
|
|
152
|
+
items.sort((a, b) => b.score - a.score || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
153
|
+
const quota = HOP_QUOTA[d] ?? 0.2;
|
|
154
|
+
const n = Math.max(1, Math.floor(depthCap * quota));
|
|
155
|
+
picked.push(...items.slice(0, n).map((x) => x.id));
|
|
156
|
+
}
|
|
157
|
+
// 队内稳定序:按深度升序(1-hop 优先),保证可感知的扩散次序。
|
|
158
|
+
picked.sort((a, b) => (depths.get(a) - depths.get(b)) || (a < b ? -1 : a > b ? 1 : 0));
|
|
159
|
+
return { ids: picked, scoreOf, depthOf: depths };
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// Lightweight self-check (ponytail: 非平凡逻辑留一个可运行断言)。
|
|
163
|
+
export function _selfCheck() {
|
|
164
|
+
const exact = normalizePath(["e1", "e2"], { path: "exact" });
|
|
165
|
+
const tag = normalizePath(["e2", "e3"], { path: "tag" });
|
|
166
|
+
assert(exact.byId.get("e1") === 1.0 && exact.byId.get("e2") === 1.0, "exact level");
|
|
167
|
+
assert(tag.byId.get("e2") === 0.4 && tag.byId.get("e3") === 0.4, "tag level");
|
|
168
|
+
const seeds = anchorSeeds({ paths: [exact, tag], cap: 10 });
|
|
169
|
+
assert(seeds[0].id === "e2", "merged highest first"); // e2: 精确+tag = 1.4
|
|
170
|
+
assert(seeds[0].score >= 1.4 - 1e-9, "score accumulates");
|
|
171
|
+
const adj = new Map([
|
|
172
|
+
["e2", ["n1", "n2"]],
|
|
173
|
+
["n1", ["n2", "deep3"]],
|
|
174
|
+
]);
|
|
175
|
+
const depths = cascadeDepths({ seeds, adjacencyOf: (id) => adj.get(id) ?? [], maxDepth: 2 });
|
|
176
|
+
assert(depths.get("n1") === 1 && depths.get("n2") === 1 && depths.get("deep3") === 2, "bfs depth");
|
|
177
|
+
const { ids } = pickByHopQuota({ depths, maxDepth: 2, depthCap: 12 });
|
|
178
|
+
assert(ids.includes("n1") && ids.includes("n2") && ids.includes("deep3"), "quota picks all");
|
|
179
|
+
return "anchoring self-check ok";
|
|
180
|
+
}
|
package/lib/guide.js
CHANGED
|
@@ -38,9 +38,21 @@ export const TOOL_GUIDE = {
|
|
|
38
38
|
" Use this when the task depends on earlier decisions, preferences, or project history that is not already in context, " +
|
|
39
39
|
"or to look for a newer memory behind one that seems stale. " +
|
|
40
40
|
"Skip it for facts you can read directly from the repository.",
|
|
41
|
+
// 第二句(#249 第二批:「能力说明里没提 scope」):TOOL_GUIDE.memory_save 此前只
|
|
42
|
+
// 讲「该不该写」,没讲「写给谁看」——而 memory_save 的 scope 参数是**必填面**,
|
|
43
|
+
// 漏填的后果是单向的。所以这里给的是一条判断规则而不是一句免责声明:不确定就别填
|
|
44
|
+
// (未标注 = 处处可见,NULL 恒可见,是安全的默认);标了 scope 才在多 scope 检索里
|
|
45
|
+
// 付出代价。那句代价必须按实写:A2 软隔离是「他 scope 降权 ×0.5 但保留可见」
|
|
46
|
+
// (service.js),只有 A3 `strictScope`(默认关)才真的硬过滤。写「静默消失」既不
|
|
47
|
+
// 符合默认配置下的行为,也撞上「拒绝可解释、不静默丢弃」的口径(#254 验收第 2 条),
|
|
48
|
+
// 会让模型以为标注有它实际没有的隐私效果。
|
|
49
|
+
// 写进工具描述而不是总则:它是 memory_save 单工具的判据,总则那五条讲的是
|
|
50
|
+
// 「何时查 / 何时写 / 何时 no-op」,加第六条会把单工具语义抬成全局纪律。
|
|
41
51
|
memory_save:
|
|
42
52
|
" Save only durable, cross-session value (a preference, a decision with its rationale, an engineering constraint, " +
|
|
43
|
-
"a pitfall with its root cause). Trivial single-turn work does not belong here, and when unsure, do not save."
|
|
53
|
+
"a pitfall with its root cause). Trivial single-turn work does not belong here, and when unsure, do not save. " +
|
|
54
|
+
"If a memory only holds for one workspace or one agent, declare workspace_scope / agent_scope; otherwise leave both out. " +
|
|
55
|
+
"An unscoped memory is visible everywhere; a scoped one is filtered or downranked outside its scope."
|
|
44
56
|
};
|
|
45
57
|
|
|
46
58
|
/**
|
package/lib/index.js
CHANGED
|
@@ -5,6 +5,8 @@ import { resolveDocumentDir } from "./document.js";
|
|
|
5
5
|
import { createService } from "./service.js";
|
|
6
6
|
// #254 写入准入(第一阶段只计量,不拦截):见 src/write-admission.js 的文件头。
|
|
7
7
|
import { createWriteAdmission } from "./write-admission.js";
|
|
8
|
+
// #164 A2:写入边界的密钥 / PII 判据,注入给上面的写入准入。
|
|
9
|
+
import { createSensitiveScan } from "./sensitive-scan.js";
|
|
8
10
|
import { createTools } from "./tools.js";
|
|
9
11
|
import { createInjector } from "./inject.js";
|
|
10
12
|
import { createContinuityRescue } from "./continuity.js";
|
|
@@ -278,11 +280,17 @@ export const apply = (ctx, config) => {
|
|
|
278
280
|
// 关掉,在无保留期的表里按写入频次增长是不能接受的)。
|
|
279
281
|
//
|
|
280
282
|
// sensitiveScan 是密钥 / PII 那一类判据的注入点。按 #254 验收第 4 条它是 #164 A2
|
|
281
|
-
//
|
|
282
|
-
//
|
|
283
|
-
//
|
|
284
|
-
//
|
|
285
|
-
|
|
283
|
+
// 的判据来源,现在由 src/sensitive-scan.js 实现(维护者 09-28 把 A2 的认领转给
|
|
284
|
+
// 本侧)。这里只做接线:判据开不开由它自己的键 sensitiveScanEnabled 决定,工厂在
|
|
285
|
+
// 关时返回 null,闸门的行为就与 #332 合并时逐字段一致(那一版根本没有这个函数);
|
|
286
|
+
// 命中之后是仅告警还是真拦,仍是 writeAdmission.enforce 的事(#164 口径:
|
|
287
|
+
// 默认仅告警、拦截 opt-in),判据不碰决策。
|
|
288
|
+
const writeAdmission = createWriteAdmission({
|
|
289
|
+
store,
|
|
290
|
+
config: cfg,
|
|
291
|
+
logger: ctx.logger,
|
|
292
|
+
sensitiveScan: createSensitiveScan({ config: cfg })
|
|
293
|
+
});
|
|
286
294
|
const service = createService({ store, mirror, config: cfg, logger: ctx.logger, documentIndex, writeAdmission });
|
|
287
295
|
|
|
288
296
|
// F-NEW-03: if the mirror sync failed last run (persisted dirty state), retry
|
|
@@ -502,6 +510,11 @@ export const apply = (ctx, config) => {
|
|
|
502
510
|
logger: ctx.logger,
|
|
503
511
|
semantic: { embedder, vectorIndex },
|
|
504
512
|
lastRunAtSeed: store.lastDreamRunAt("auto"),
|
|
513
|
+
// Issue #89(基线半边):阈值基线同样跨重启恢复——只恢复 lastRunAt 的话,
|
|
514
|
+
// 重启后的第一条写入仍会绕过阈值(库里记忆多于阈值时 overBase 恒成立)开跑
|
|
515
|
+
// 整轮。种子取上次成功轮的库规模,口径与运行期刷新一致;读不到时调度器退回
|
|
516
|
+
// 零基线(= 升级前行为)。
|
|
517
|
+
baselineSeed: store.lastDreamBaseline("auto"),
|
|
505
518
|
// Issue #239(第 4 项)镜像到巩固:高峰期不做梦,顺延到最近的高峰结束时刻。
|
|
506
519
|
peakHours: cfg.dreamPeakHours ?? "",
|
|
507
520
|
peakMaxDeferMinutes: cfg.dreamPeakMaxDeferMinutes ?? 120,
|