@modusensus/dsh-mneme 0.8.11 → 0.8.12
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/config.js +47 -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/index.js +5 -0
- package/lib/inject.js +15 -3
- package/lib/lang.js +10 -0
- package/lib/service.js +164 -23
- package/lib/settings.js +24 -2
- 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 +7 -6
- package/scripts/sync-test-badge.mjs +9 -7
- package/scripts/test-count-sync.mjs +44 -0
- package/src/config.js +47 -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/index.js +5 -0
- package/src/inject.js +15 -3
- package/src/lang.js +10 -0
- package/src/service.js +164 -23
- package/src/settings.js +24 -2
- package/src/store.js +155 -11
- package/src/summarize.js +104 -7
- package/test/api.test.js +7 -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/scope-inject-soft.test.js +106 -0
- package/test/summarize.test.js +177 -0
- package/test/test-count-sync.test.js +60 -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/ # 1497 个 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 # 运行 1497 个测试
|
|
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/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)
|
|
@@ -685,7 +722,8 @@ export const Config = z.object({
|
|
|
685
722
|
// agent_scope, workspace_scope, sensitivity)。检索侧加权/过滤在 A2/A3 落地。
|
|
686
723
|
// 也走 feature_flags(FEATURE_FLAG_BOOLEANS 白名单),面板可启停=线上回滚开关。
|
|
687
724
|
scopeEnabled: z.boolean().default(false),
|
|
688
|
-
// strictScope(A3):硬过滤模式。关闭=A2 软隔离(他 scope
|
|
725
|
+
// strictScope(A3):硬过滤模式。关闭=A2 软隔离(他 scope 降权保留可见,
|
|
726
|
+
// 检索与注入排序同权重,issue #339 补齐注入通道);
|
|
689
727
|
// 开启后检索/注入/list/get 按 issue #17 四象限可见性公式硬过滤——带他 scope
|
|
690
728
|
// 的记忆完全不可见,未标注(NULL)恒可见;当前会话某维度解析不到时该维度
|
|
691
729
|
// 带标注的记忆一律不可见(fail-closed:身份不明只见全局)。依赖 scopeEnabled
|
|
@@ -712,6 +750,14 @@ const LIGHT_MODE_OFF = [
|
|
|
712
750
|
"bm25SearchEnabled",
|
|
713
751
|
// 轻量模式不开图召回轴(#219,依赖实体抽取产出;抽取本身已被关掉)。
|
|
714
752
|
"entityRecallEnabled",
|
|
753
|
+
// 轻量模式不开图谱锚定层级联(#24 块1,依赖实体抽取 + 关系表,抽取已关则无边可扩)。
|
|
754
|
+
"graphAnchoringEnabled",
|
|
755
|
+
// 轻量模式不开边权重演化(#24 块2,依赖实体抽取产出;抽取关则无 v08 边可抬升)。
|
|
756
|
+
"graphWeightEnabled",
|
|
757
|
+
// 轻量模式不开关联提示标注(#24 块3,常驻注入文本多一段提示成本)。
|
|
758
|
+
"graphInjectHint",
|
|
759
|
+
// 轻量模式不开被动确认(#24 块4,依赖实体抽取产出;抽取关则无 v08 边可抬升)。
|
|
760
|
+
"graphPassiveConfirm",
|
|
715
761
|
// 轻量模式不开叙述条(额外 LLM 调用;#164 对齐,opt-in)。
|
|
716
762
|
"dreamNarrativeEnabled",
|
|
717
763
|
// 轻量模式不开 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/index.js
CHANGED
|
@@ -502,6 +502,11 @@ export const apply = (ctx, config) => {
|
|
|
502
502
|
logger: ctx.logger,
|
|
503
503
|
semantic: { embedder, vectorIndex },
|
|
504
504
|
lastRunAtSeed: store.lastDreamRunAt("auto"),
|
|
505
|
+
// Issue #89(基线半边):阈值基线同样跨重启恢复——只恢复 lastRunAt 的话,
|
|
506
|
+
// 重启后的第一条写入仍会绕过阈值(库里记忆多于阈值时 overBase 恒成立)开跑
|
|
507
|
+
// 整轮。种子取上次成功轮的库规模,口径与运行期刷新一致;读不到时调度器退回
|
|
508
|
+
// 零基线(= 升级前行为)。
|
|
509
|
+
baselineSeed: store.lastDreamBaseline("auto"),
|
|
505
510
|
// Issue #239(第 4 项)镜像到巩固:高峰期不做梦,顺延到最近的高峰结束时刻。
|
|
506
511
|
peakHours: cfg.dreamPeakHours ?? "",
|
|
507
512
|
peakMaxDeferMinutes: cfg.dreamPeakMaxDeferMinutes ?? 120,
|
package/lib/inject.js
CHANGED
|
@@ -274,9 +274,14 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
274
274
|
// 日志抢同一份预算后被降级」正是本议题要修的结构缺陷。单条仍有硬顶
|
|
275
275
|
// (PINNED_CONTENT_MAX),超出照旧带截断提示,不静默。
|
|
276
276
|
const pinnedCount = Math.max(0, Math.min(candidates.length, pinnedStats?.shown ?? 0));
|
|
277
|
+
// #24 块3:图召回线索行(graphHint)——引导语插在第一个线索行之前(保持块头
|
|
278
|
+
// 语义:先声明「这些不是事实」)。线索行自身按 graphHintLine 渲染(线索/ 前缀
|
|
279
|
+
// 区别于普通记忆行)。
|
|
280
|
+
let graphSeen = 0;
|
|
277
281
|
for (let i = 0; i < candidates.length; i++) {
|
|
278
282
|
const m = candidates[i];
|
|
279
283
|
const pinned = i < pinnedCount;
|
|
284
|
+
if (m.graphHint === true) graphSeen++;
|
|
280
285
|
// Epistemic trust (v0.4.5): when enabled, measured observations are
|
|
281
286
|
// flagged so the agent can weigh them above guesses/opinions.
|
|
282
287
|
const verified = config.trustEpistemicWeighting === true && m.epistemic_status === "observation"
|
|
@@ -284,7 +289,13 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
284
289
|
: "";
|
|
285
290
|
const title = STR.entryTitle[language](m.title, m.importance);
|
|
286
291
|
const content = injectMemory(m, pinned ? PINNED_CONTENT_MAX : maxContent);
|
|
287
|
-
const full =
|
|
292
|
+
const full = m.graphHint === true
|
|
293
|
+
? STR.graphHintLine[language](m.type, title, content)
|
|
294
|
+
: STR.entryLine[language](m.type, verified, title, content);
|
|
295
|
+
// 线索行引导语插在第一个线索行之前(保持块头语义:先声明「这些不是事实」)。
|
|
296
|
+
if (m.graphHint === true && graphSeen === 1) {
|
|
297
|
+
lines.push(STR.graphHintHeader[language]);
|
|
298
|
+
}
|
|
288
299
|
if (pinned) {
|
|
289
300
|
// pin 不扣块预算(上面那条设计注释的落地):pin 一条就够击穿 MAX_BLOCK
|
|
290
301
|
// (PINNED_CONTENT_MAX 2000 > MAX_BLOCK 1500),照扣会把 budget 压成负数,
|
|
@@ -363,8 +374,9 @@ export function createInjector(ctx, service, settings, config) {
|
|
|
363
374
|
const query = lastUserQuery(ctx);
|
|
364
375
|
if (query) prefetchQueryVector(query);
|
|
365
376
|
const queryVector = queryVectorCache.get(query);
|
|
366
|
-
// v0.8.0 A3:scope
|
|
367
|
-
//
|
|
377
|
+
// v0.8.0 A3:scope 随请求解析(解析器恒返回对象,身份取不到时两维为
|
|
378
|
+
// null);硬过滤与软加权的门控都在 injectCandidates 内——strict 看
|
|
379
|
+
// strictScope,软加权看 scopeEnabled 且至少一维可解析(issue #339)。
|
|
368
380
|
const scope = resolveSessionScope(ctx);
|
|
369
381
|
// Issue #205:跨轮轮换——最近 N 个查询轮次注入过的 id 本轮不再优先。
|
|
370
382
|
const sessionId = ctx?.agent?.session?.id ?? "_";
|
package/lib/lang.js
CHANGED
|
@@ -81,6 +81,16 @@ export const STR = {
|
|
|
81
81
|
zh: (n) => `〔约束/偏好类另有 ${n} 条未展示〕`,
|
|
82
82
|
en: (n) => `[${n} more constraint/preference entries not shown]`
|
|
83
83
|
},
|
|
84
|
+
// Issue #24 块3:图召回线索行的引导语——正向声明「链路信息非事实断言」,
|
|
85
|
+
// 防 LLM 单次消费把桥接召回当有依据的事实回头扩散(幻觉诱导)。
|
|
86
|
+
graphHintHeader: {
|
|
87
|
+
zh: "[检索线索] 以下为实体图搜索的链路信息,仅供定位参考,非事实断言:",
|
|
88
|
+
en: "[Retrieval hints] Entity-graph link trails below are navigation aids, not factual assertions:"
|
|
89
|
+
},
|
|
90
|
+
graphHintLine: {
|
|
91
|
+
zh: (type, title, content) => `- [线索/${type}] ${title}:${content}`,
|
|
92
|
+
en: (type, title, content) => `- [hint/${type}] ${title}: ${content}`
|
|
93
|
+
},
|
|
84
94
|
userSettingsHeader: {
|
|
85
95
|
zh: "[用户设置] 来自 dsh-mneme 的用户画像与规则:",
|
|
86
96
|
en: "[User settings] Profile and rules from dsh-mneme:"
|