@modusensus/dsh-mneme 0.8.4 → 0.8.6

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.
Files changed (75) hide show
  1. package/README.md +45 -8
  2. package/bin/dsh-mneme-mcp.mjs +11 -3
  3. package/lib/api-standalone.js +11 -0
  4. package/lib/api.js +21 -0
  5. package/lib/client.js +213 -31
  6. package/lib/config.js +106 -5
  7. package/lib/document.js +221 -0
  8. package/lib/dream/sleep.js +128 -12
  9. package/lib/dream.js +46 -11
  10. package/lib/guide.js +44 -0
  11. package/lib/heat.js +2 -0
  12. package/lib/index.js +65 -3
  13. package/lib/inject.js +91 -15
  14. package/lib/lang.js +6 -0
  15. package/lib/local-embedder.js +44 -6
  16. package/lib/organize.js +450 -0
  17. package/lib/recall-stats.js +23 -1
  18. package/lib/runtime/layout.js +30 -0
  19. package/lib/runtime/loader.js +4 -1
  20. package/lib/runtime/verify.js +19 -6
  21. package/lib/search/adaptive.js +38 -0
  22. package/lib/service.js +165 -12
  23. package/lib/settings.js +35 -4
  24. package/lib/store.js +126 -8
  25. package/lib/summarize.js +253 -8
  26. package/lib/tools.js +136 -8
  27. package/package.json +6 -6
  28. package/runtime-manifest.json +104 -174
  29. package/scripts/build-runtime-manifest.mjs +8 -1
  30. package/scripts/sync-test-badge.mjs +18 -12
  31. package/src/api-standalone.js +11 -0
  32. package/src/api.js +21 -0
  33. package/src/config.js +106 -5
  34. package/src/document.js +221 -0
  35. package/src/dream/sleep.js +128 -12
  36. package/src/dream.js +46 -11
  37. package/src/guide.js +44 -0
  38. package/src/heat.js +2 -0
  39. package/src/index.js +65 -3
  40. package/src/inject.js +91 -15
  41. package/src/lang.js +6 -0
  42. package/src/local-embedder.js +44 -6
  43. package/src/organize.js +450 -0
  44. package/src/recall-stats.js +23 -1
  45. package/src/runtime/layout.js +30 -0
  46. package/src/runtime/loader.js +4 -1
  47. package/src/runtime/verify.js +19 -6
  48. package/src/search/adaptive.js +38 -0
  49. package/src/service.js +165 -12
  50. package/src/settings.js +35 -4
  51. package/src/store.js +126 -8
  52. package/src/summarize.js +253 -8
  53. package/src/tools.js +136 -8
  54. package/test/adaptive-inject-budget.test.js +85 -0
  55. package/test/api.test.js +66 -3
  56. package/test/client.test.js +63 -2
  57. package/test/document.test.js +523 -0
  58. package/test/dream.test.js +51 -0
  59. package/test/inject-content-cap.test.js +5 -3
  60. package/test/inject-pools.test.js +170 -0
  61. package/test/inject-recall.test.js +105 -0
  62. package/test/inject.test.js +35 -1
  63. package/test/lastrunat-persist.test.js +101 -0
  64. package/test/llm-audit.test.js +247 -0
  65. package/test/local-embedder.test.js +45 -2
  66. package/test/organize.test.js +418 -0
  67. package/test/recall-stats.test.js +7 -1
  68. package/test/runtime-loader.test.js +18 -0
  69. package/test/runtime-manifest.test.js +2 -1
  70. package/test/runtime-verify.test.js +65 -0
  71. package/test/sleep.test.js +154 -0
  72. package/test/store.test.js +13 -0
  73. package/test/summarize.test.js +407 -3
  74. package/test/tools-dto-schema-matrix.test.js +325 -0
  75. package/test/tools.test.js +36 -3
package/src/service.js CHANGED
@@ -7,16 +7,30 @@ import { computeHeat } from "./heat.js";
7
7
  import { recallStats } from "./recall-stats.js";
8
8
  import { evaluateMemoryQuality } from "./quality-filter.js";
9
9
  import { applyDecisions } from "./dream/decisions.js";
10
+ import { createDocumentRegistrar } from "./document.js";
11
+ import { createOrganizer } from "./organize.js";
10
12
  import { createBM25Index } from "./search/bm25.js";
11
13
  import { adaptiveThreshold } from "./search/adaptive.js";
12
14
 
13
15
  const INJECT_TYPES = new Set(["preference", "project", "decision", "summary", "rejected_solution", "pitfall", "constraint"]);
14
16
 
17
+ // document 摘要行(#230):仅 documentMemoryEnabled 开启时进注入候选。独立
18
+ // 集合而非常改共享 Set——INJECT_TYPES 本身是「类型可注入」的静态事实,flag
19
+ // 是运行时状态,两者不能搅在一起。
20
+ const INJECT_TYPES_WITH_DOCUMENT = new Set([...INJECT_TYPES, "document"]);
21
+
15
22
  // 编码记忆类型(codingRetrospect):rejected_solution / pitfall / constraint
16
23
  // 只在编码任务时注入(防噪声污染其他业务),且编码场景下按 codingBoostFactor
17
24
  // 加权排序提前。
18
25
  const CODING_MEMORY_TYPES = new Set(["rejected_solution", "pitfall", "constraint"]);
19
26
 
27
+ // pin 池类型(#249 第一批):约束与偏好是「对谁都成立的边界」,需要逐字保真
28
+ // 而不是相关性排序——它们与情景日志同池同速率摘要会被静默降级(立项依据)。
29
+ // 注意 constraint 同时属于 CODING_MEMORY_TYPES,非编码任务里已被 codingGate
30
+ // 滤掉,pin 池同样拿不到它;是否豁免该门控是待维护者拍板的口径问题,本批次
31
+ // 不动既有门控(不放大行为面)。
32
+ const PINNED_MEMORY_TYPES = new Set(["constraint", "preference"]);
33
+
20
34
  /**
21
35
  * 判断一段文本是否编码类任务(关键词匹配,codingRetrospect 读取侧门控)。
22
36
  * 纯函数,无副作用,便于单测。
@@ -39,8 +53,10 @@ const CONTENT_HISTORY_MAX = 20;
39
53
 
40
54
  // Issue #135 附属发现 2:质量过滤器写下的系统信号标签(「为什么被降权/归档」的
41
55
  // 唯一审计线索)。更新路径整组替换 tags 会把它们抹掉——更新时按此清单并集保留。
42
- // 清单必须与 quality-filter.js 的写入端对齐(6 个,含 type 自指的 self_referential)。
43
- const SIGNAL_TAGS = ["low_quality", "duplicate", "meta", "repetitive", "short_content", "self_referential"];
56
+ // 清单必须与 quality-filter.js 的写入端对齐(6 个,含 type 自指的 self_referential);
57
+ // evidence_degraded 是 #230 注册端的求交标记(quality-filter 不写它,但同样
58
+ // 不许被更新抹掉)。
59
+ const SIGNAL_TAGS = ["low_quality", "duplicate", "meta", "repetitive", "short_content", "self_referential", "evidence_degraded"];
44
60
 
45
61
  // scopeKeyOf 已上提到 ./scope.js(v0.8.1,issue #170 第 2 步):与 sleep 的跨
46
62
  // scope 配对共用同一把比较钥匙,避免两处定义漂移。
@@ -1110,6 +1126,13 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1110
1126
  * @returns {{action: "created"|"merged", memory: object}}
1111
1127
  */
1112
1128
  function saveWithDedupe(memory) {
1129
+ // #230 写入权分离:document 行只能经 registerDocument 铸造(注册校验 +
1130
+ // doc_path + evidence 三样俱全)。通用保存路径(memory_save 工具 / MCP /
1131
+ // standalone API / bootstrap)一律拒绝——防止绕过注册校验造出无指针无
1132
+ // 回链的伪 document 行。与 updateMemory 的同款守卫构成双保险。
1133
+ if (memory?.type === "document") {
1134
+ throw new Error("type 'document' is minted only via registerDocument (summary + doc_path + evidence)");
1135
+ }
1113
1136
  // Bug7: score quality once (after dedupe lookup, before write). Failures
1114
1137
  // inside the evaluator are impossible (pure function), but the write that
1115
1138
  // records the score must never fail the save — wrap defensively.
@@ -1301,8 +1324,13 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1301
1324
  * lead the selection (up to maxItems*2 candidates) and the rule-based pick
1302
1325
  * fills + dedupes the remaining slots. Empty query / no cached recall /
1303
1326
  * hybridInject off → pure legacy rule-based selection.
1327
+ *
1328
+ * #249 第一批(B1 pin 池):`pinnedInjectBudget` > 0 时,约束/偏好类先按相关性
1329
+ * 取满独立预算、再从候选里摘除(于是轮换重排碰不到它们),由调用方前置到块
1330
+ * 头。`pinnedStats` 是可选出参:回报实际 pin 条数与超预算未展示条数,不改变
1331
+ * 本函数「返回数组」的既有契约。预算为 0 时整段不执行,行为逐字节不变。
1304
1332
  */
1305
- function injectCandidates({ query = "", maxItems = 5, threshold = 3, queryVector, scope = null, rotate = null, rotateWindow = 0 } = {}) {
1333
+ function injectCandidates({ query = "", maxItems = 5, threshold = 3, queryVector, scope = null, rotate = null, rotateWindow = 0, pinnedStats = null } = {}) {
1306
1334
  const q = String(query ?? "").trim();
1307
1335
  // codingRetrospect 读取侧门控:编码记忆(rejected_solution / pitfall /
1308
1336
  // constraint)只在编码任务时注入,防噪声污染其他业务;编码任务时按
@@ -1321,11 +1349,19 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1321
1349
  const poolSize = rotateWindowN > 0
1322
1350
  ? Math.max(maxItems * 2, maxItems * (rotateWindowN + 1))
1323
1351
  : maxItems * 2;
1352
+ // #230 拍板:注入档位合并设计(叙述条次优先档 + document 摘要行预算)。
1353
+ // ①叙述条(source=narrative)从纯按需解禁进注入候选,落次优先档——但受
1354
+ // 生成总闸 dreamNarrativeEnabled 约束(flag 关 = 该类行不再注入,存量行
1355
+ // 仍可检索);②document 摘要行同落次优先档,另有独立预算(见下方选取)。
1356
+ const injectTypes = config?.documentMemoryEnabled === true ? INJECT_TYPES_WITH_DOCUMENT : INJECT_TYPES;
1357
+ // #230 拍板:叙述条解禁进注入受生成总闸约束——门必须对全部候选路径一致
1358
+ // (规则/向量/缓存/BM25),只锁规则路的话语义路仍会漏进 narrative 行。
1359
+ const allowNarrative = config?.dreamNarrativeEnabled === true;
1324
1360
  const filtered = store.list({ limit: Math.max(200, poolSize), includeForgotten: false })
1325
- .filter((m) => !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten &&
1326
- // 叙述条(#164 对齐)按需检索:source=narrative 的 per-topic 叙述不进
1327
- // 注入候选——常驻位只留给 dream 总览(source=dream)。
1328
- m.source !== "narrative" &&
1361
+ .filter((m) => !m.archived && injectTypes.has(m.type) && !m.forgotten &&
1362
+ // 叙述条:#228 落地为纯按需检索;#230 合并拍板解禁为次优先档注入
1363
+ // (per-topic 叙述常驻位仍只留给 dream 总览 source=dream)。
1364
+ (m.source !== "narrative" || allowNarrative) &&
1329
1365
  codingGate(m) &&
1330
1366
  (m.type === "summary" || m.type === "preference" || m.importance >= threshold));
1331
1367
  // #218 v1: heat 乘数——heatEnabled 时在优先级层内给 importance×quality 乘
@@ -1341,8 +1377,13 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1341
1377
  // 编码记忆在编码任务时优先于普通 decision(与 preference 同级),
1342
1378
  // importance 乘 codingBoostFactor 加权(封顶 5,保持 importance 语义)。
1343
1379
  const priority = (m) => {
1380
+ // #230:叙述条与 document 摘要行同为次优先档——蒸馏摘要(0)仍最
1381
+ // 先,指针型聚合产物(1)先于普通 project/decision(2)。叙述条是
1382
+ // type=summary,判 source 必须在判 type 之前。
1383
+ if (m.source === "narrative") return 1;
1344
1384
  if (m.type === "summary") return 0;
1345
1385
  if (m.type === "preference") return 1;
1386
+ if (m.type === "document") return 1;
1346
1387
  if (isCoding && CODING_MEMORY_TYPES.has(m.type)) return 1;
1347
1388
  return 2;
1348
1389
  };
@@ -1367,7 +1408,8 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1367
1408
  try {
1368
1409
  const hits = vectorIndex.search(queryVector, { limit: poolSize, threshold: 0 });
1369
1410
  for (const m of hits) {
1370
- if (m && !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten &&
1411
+ if (m && !m.archived && injectTypes.has(m.type) && !m.forgotten &&
1412
+ (m.source !== "narrative" || allowNarrative) &&
1371
1413
  codingGate(m) &&
1372
1414
  (m.type === "summary" || m.type === "preference" || m.importance >= threshold)) {
1373
1415
  semanticItems.push(m);
@@ -1377,7 +1419,8 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1377
1419
  }
1378
1420
  if (!semanticItems.length && lastSemanticRecall?.query === q && lastSemanticRecall.items?.length) {
1379
1421
  for (const m of lastSemanticRecall.items) {
1380
- if (m && !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten && codingGate(m)) semanticItems.push(m);
1422
+ if (m && !m.archived && injectTypes.has(m.type) && !m.forgotten &&
1423
+ (m.source !== "narrative" || allowNarrative) && codingGate(m)) semanticItems.push(m);
1381
1424
  }
1382
1425
  }
1383
1426
  // Issue #198:首轮(无向量、无缓存召回)的同步兜底——BM25 词法召回领位。
@@ -1389,7 +1432,8 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1389
1432
  // 语义向量命中时本分支不参与,行为不变。
1390
1433
  if (!semanticItems.length) {
1391
1434
  for (const hit of bm25Recall(q, poolSize)) {
1392
- if (hit && !hit.archived && INJECT_TYPES.has(hit.type) && !hit.forgotten &&
1435
+ if (hit && !hit.archived && injectTypes.has(hit.type) && !hit.forgotten &&
1436
+ (hit.source !== "narrative" || allowNarrative) &&
1393
1437
  codingGate(hit) &&
1394
1438
  (hit.type === "summary" || hit.type === "preference" || hit.importance >= threshold)) {
1395
1439
  semanticItems.push(hit);
@@ -1434,6 +1478,20 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1434
1478
  if (config?.strictScope === true && scope) {
1435
1479
  candidates = candidates.filter((m) => isVisibleInScope(m, scope));
1436
1480
  }
1481
+ // #249 第一批:B1 pin 池。取在相关性排序之后、轮换之前——取谁按此刻的候选
1482
+ // 次序(即相关性次序),取到后从候选中摘除,于是下面的轮换重排碰不到它们
1483
+ // (验收:pin 不参与跨轮轮换)。独立预算的两层意义:pin 既不占 maxItems
1484
+ // 名额、也不被 document 预算截断,因此不会把当前任务需要的情景候选挤出去;
1485
+ // 超预算的条数回报给调用方,在块内如实标注(绝不静默省略)。
1486
+ const pinnedBudget = Math.max(0, Math.min(5, Math.floor(config?.pinnedInjectBudget ?? 0)));
1487
+ // eligible 留到块外:未展示条数要等 general 槽选完才算得准(见 selected 之后)。
1488
+ const eligible = pinnedBudget > 0 ? candidates.filter((m) => PINNED_MEMORY_TYPES.has(m.type)) : [];
1489
+ let pinned = [];
1490
+ if (pinnedBudget > 0 && eligible.length > 0) {
1491
+ pinned = eligible.slice(0, pinnedBudget);
1492
+ const pinnedIds = new Set(pinned.map((m) => m.id));
1493
+ candidates = candidates.filter((m) => !pinnedIds.has(m.id));
1494
+ }
1437
1495
  // Issue #205:注入位跨轮轮换。rotate = 最近 N 轮注入过的 id 集合(由注入层
1438
1496
  // 按会话维护并传入):这些条目本轮不再优先——新鲜者前置(各自内部相对次序
1439
1497
  // 保持),不足时按原序回填,槽位数与 touchRecall 语义均不变。rotate 为空时
@@ -1442,8 +1500,58 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1442
1500
  const fresh = candidates.filter((m) => !rotate.has(m.id));
1443
1501
  if (fresh.length > 0) candidates = [...fresh, ...candidates.filter((m) => rotate.has(m.id))];
1444
1502
  }
1445
- const selected = candidates.slice(0, maxItems);
1503
+ // #230 拍板:document 摘要行预算——次优先档内最多 documentInjectBudget
1504
+ // 条,超预算的 document 行跳过、由后续候选补位(不占槽)。预算只约束
1505
+ // 注入不约束检索:指针行价值在「按需读全文」,常驻注入若不设界就会把
1506
+ // 注入块变成文档目录(#164 失败判据:批量把历史塞进上下文)。
1507
+ const documentBudget = config?.documentInjectBudget ?? 2;
1508
+ let documentSeen = 0;
1509
+ const general = [];
1510
+ for (const m of candidates) {
1511
+ if (general.length >= maxItems) break;
1512
+ if (m.type === "document") {
1513
+ if (documentSeen >= documentBudget) continue;
1514
+ documentSeen++;
1515
+ }
1516
+ general.push(m);
1517
+ }
1518
+ // #249 第一批:pin 前置到块内相关性排序之前(验收项),且不占 maxItems 名额。
1519
+ // pinned 为空时 selected 就是 general 本身——关闭态与改动前逐字节一致。
1520
+ const selected = pinned.length > 0 ? [...pinned, ...general] : general;
1521
+ if (pinnedStats) {
1522
+ // 「未展示」只数**真的没进块**的 pin 类条目:被 pin 预算挤下来的条目会回到
1523
+ // 候选池,仍可能被 general 槽选中——那就是展示了。按 eligible - pinned 直接
1524
+ // 相减会把它们也算成未展示,块头那一行于是虚报(评审实测:pref#2 已在块内,
1525
+ // 仍报「另有 2 条未展示」)。所以统一按「有没有进 selected」判。
1526
+ const shownIds = new Set(selected.map((m) => m.id));
1527
+ pinnedStats.shown = pinned.length;
1528
+ pinnedStats.suppressed = eligible.filter((m) => !shownIds.has(m.id)).length;
1529
+ }
1446
1530
  touchRecalled(selected);
1531
+ // #217 口径(2026-09-19 拍板):注入是曝光型访问事件,与检索命中同表分账
1532
+ // (mode='inject',candidates 存实际注入集)。跟随 recallRecordDefault——
1533
+ // 与检索侧同门,不设新配置键;heat 关闭时照写,留痕与消费解耦(两 issue
1534
+ // 独立验收)。空选不记(没有访问发生);记账失败不影响注入本身。
1535
+ if ((config?.recallRecordDefault ?? true) && recallRecorder && selected.length > 0) {
1536
+ try {
1537
+ recallRecorder({
1538
+ // recall_runs.query 是 NOT NULL(store.js:61):注入是主动曝光、
1539
+ // 没有查询词,用空串而非 null——SQLite 改列约束需重建表,不值得。
1540
+ query: "",
1541
+ mode: "inject",
1542
+ topK: maxItems,
1543
+ threshold: null,
1544
+ candidates: selected.map((m) => ({
1545
+ id: m.id,
1546
+ title: m.title,
1547
+ content: m.content,
1548
+ score: m.score ?? null,
1549
+ source: m.source ?? "keyword"
1550
+ })),
1551
+ createdAt: new Date().toISOString()
1552
+ });
1553
+ } catch { /* recall receipt is best effort */ }
1554
+ }
1447
1555
  return selected;
1448
1556
  }
1449
1557
 
@@ -1604,7 +1712,10 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1604
1712
  ...(m.workspace_scope_source !== undefined ? { workspace_scope_source: m.workspace_scope_source } : {}),
1605
1713
  ...(m.scope_decided_at !== undefined ? { scope_decided_at: m.scope_decided_at } : {}),
1606
1714
  ...(m.sensitivity !== undefined ? { sensitivity: m.sensitivity } : {}),
1607
- ...(m.occurred_at !== undefined ? { occurred_at: m.occurred_at } : {})
1715
+ ...(m.occurred_at !== undefined ? { occurred_at: m.occurred_at } : {}),
1716
+ // #230:document 指针行的文件定位随行透出——「全文按需读」的入口就是
1717
+ // 这个路径;普通行恒不带键,DTO 与 #230 前逐字节同形。
1718
+ ...(m.doc_path !== undefined && m.doc_path !== null ? { doc_path: m.doc_path } : {})
1608
1719
  }));
1609
1720
  }
1610
1721
 
@@ -1889,6 +2000,12 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1889
2000
  // 处置)共用的写路径:mirror 同步、写通知、嵌入调度全部同源,不能各写一份。
1890
2001
  function updateMemory(id, p, ctx = {}) {
1891
2002
  const old = store.getById(id);
2003
+ // #230 写入权分离(与 saveWithDedupe 同款守卫):document 行只能经
2004
+ // registerDocument 铸造。既有 document 行的摘要修复(memory_update 改
2005
+ // content/title——设计定案的更新通道)照常放行,type 不许改入 document。
2006
+ if (p?.type === "document" && old?.type !== "document") {
2007
+ throw new Error("type 'document' is minted only via registerDocument (summary + doc_path + evidence)");
2008
+ }
1892
2009
  // Issue #135 附属发现 2:质量过滤器把「为什么被降权/归档」写在系统信号标签
1893
2010
  // 上(SIGNAL_TAGS,applyQualityDisposition 以并集写入),而这里整组替换
1894
2011
  // tags——任何带 tags 的更新都会抹掉这条唯一的审计线索(报告实测 150 条低分
@@ -1978,8 +2095,42 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1978
2095
  return updated;
1979
2096
  }
1980
2097
 
2098
+ // document 型记忆注册(#230):内聚块在 src/document.js(AGENTS.md 尺寸
2099
+ // 约定,同 recallStats 先例),这里只做依赖注入 + barrel 出口,调用方零改动。
2100
+ // 写后语只做重嵌入:镜像同步与写通知由 transaction 的 commit 路径统一执行
2101
+ // (notifyWrite 在 txDepth>0 时 deferred 到 finally),这里再调就是双份。
2102
+ const registerDocument = createDocumentRegistrar({
2103
+ store,
2104
+ config,
2105
+ embedQuery,
2106
+ pushContentHistory,
2107
+ transaction,
2108
+ finalize: (rows) => {
2109
+ for (const row of rows) scheduleEmbed(row);
2110
+ }
2111
+ });
2112
+
2113
+ // agent 主动整理接口(#231):dryRun 比对报告 → agent 判断 → apply 落库,筛除项
2114
+ // 进归档不删,全程复用 dream_runs 的 receipt 语义(不新建审计面)。走
2115
+ // saveWithDedupe 落库 = 复用常规写路径的镜像/通知语;重嵌入由 finalize 在事务
2116
+ // 提交后补(事务里的 scheduleEmbed 被 txDepth 挡掉)——document 同款先例。
2117
+ const { organize } = createOrganizer({
2118
+ store,
2119
+ embedQuery,
2120
+ saveWithDedupe,
2121
+ transaction,
2122
+ finalize: (rows) => {
2123
+ for (const row of rows) scheduleEmbed(row);
2124
+ }
2125
+ });
2126
+
1981
2127
  return {
1982
2128
  saveWithDedupe,
2129
+ registerDocument,
2130
+ // agent 主动整理接口(#231):内聚块在 src/organize.js,这里只做依赖注入 +
2131
+ // barrel 出口。刻意不加 opt-in 开关(维护者口径:功能本体不做开关,与 #249
2132
+ // 同批暴露时再定配置面),也不进工具列表——工具注册在 #249 那批。
2133
+ organize,
1983
2134
  recoverMirror,
1984
2135
  getMirrorHealth,
1985
2136
  getMirrorState: () => store.getMirrorState(),
@@ -1988,6 +2139,8 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1988
2139
  toApiList,
1989
2140
  isVisibleInScope,
1990
2141
  transaction,
2142
+ getDistillCursor: (sessionId) => store.getDistillCursor(sessionId),
2143
+ setDistillCursor: (sessionId, lastSeq) => store.setDistillCursor(sessionId, lastSeq),
1991
2144
  enqueue,
1992
2145
  setDreamHook(fn) { dreamHook = fn; },
1993
2146
  setSleepHook(fn) { sleepHook = fn; },
package/src/settings.js CHANGED
@@ -43,6 +43,10 @@ const FEATURE_FLAG_BOOLEANS = [
43
43
  "autoInject",
44
44
  "autoSummarize",
45
45
  "hotMemoryEnabled",
46
+ // Issue #239 第 5 项:注入条数的查询自适应(确定性强则收缩注入条数,默认关)。
47
+ "injectUncertaintyAdaptive",
48
+ // Issue #249 第一批:能力说明(工具描述判断指引 + order 150 总则段,默认关)。
49
+ "injectGuidanceEnabled",
46
50
  "entityExtractionEnabled",
47
51
  // Issue #219:图召回轴——查询命中实体名时把挂联记忆并入检索融合池
48
52
  // (默认关;依赖实体抽取产出,lightMode 强制关闭)。
@@ -50,6 +54,9 @@ const FEATURE_FLAG_BOOLEANS = [
50
54
  // Issue #164:叙述条——dream 期间按 tag 主题簇合成叙述落库(source=
51
55
  // narrative,evidence 回链簇内记忆;按需检索不常驻注入;默认关)。
52
56
  "dreamNarrativeEnabled",
57
+ // Issue #230:document 型记忆——agent 产长文档的指针行(注册校验/摘要+
58
+ // doc_path 落库/C2 去重/supersede 记账;全文归 agent;默认关,lightMode 强制关)。
59
+ "documentMemoryEnabled",
53
60
  "codingRetrospect",
54
61
  "autoDream",
55
62
  "sleepModeEnabled",
@@ -78,6 +85,10 @@ const FEATURE_FLAG_BOOLEANS = [
78
85
  // Issue #17(v0.8.0 A3):strictScope 硬过滤——他 scope 完全不可见(关闭时
79
86
  // 为 A2 软隔离:降权保留可见)。
80
87
  "strictScope",
88
+ // 工具暴露开关(v0.8.5):记忆已每轮自动注入,memory_search/memory_archive
89
+ // 在慢/轻量模型上是多余往返,面板可关(默认关=行为不变)。
90
+ "disableMemorySearch",
91
+ "disableMemoryArchive",
81
92
  // 嵌套对象开关:config.js 里是 memoryQualityFilter / llmAudit 对象的 enabled
82
93
  // 子字段。kv 按点号键平铺存("memoryQualityFilter.enabled": false),index.js
83
94
  // 合并时展开回嵌套对象,api.js 的 effective 从对象子字段取值。
@@ -97,12 +108,25 @@ const FEATURE_FLAG_INT_RANGES = {
97
108
  summarizeMinIntervalMinutes: [0, 10080],
98
109
  summarizeMaxEntriesPerRun: [0, 50],
99
110
  summarizeDedupeWindowHours: [0, 168],
111
+ // Issue #239:蒸馏零 LLM 预判(窗口最小字符数)与每会话 run 预算(0 = 零行为变化)。
112
+ summarizeMinWindowChars: [0, 100000],
113
+ summarizeMaxRunsPerSession: [0, 1000],
114
+ // Issue #239 第 4 项:高峰顺延上限(分钟,0 = 不设上限)。
115
+ summarizePeakMaxDeferMinutes: [0, 1440],
100
116
  // Issue #125:hybrid 候选量上限(0 = 复用 dreamMaxSnapshotSize)。
101
117
  dreamCandidateMax: [0, 5000],
102
118
  // Issue #164①:注入单条正文截断上限(默认 300 = 既有行为)。
103
119
  injectContentMaxChars: [60, 4000],
104
120
  // Issue #164:叙述条成簇门槛(共享同一 tag 的记忆数下限)。
105
- dreamNarrativeMinCluster: [2, 20]
121
+ dreamNarrativeMinCluster: [2, 20],
122
+ // Issue #230:document 摘要行的注入预算(次优先档内最多几条指针行)。
123
+ documentInjectBudget: [1, 5],
124
+ // Issue #249 第一批:B1 pin 池(约束/偏好)的独立条数预算(0 = 关闭/现状)。
125
+ pinnedInjectBudget: [0, 5],
126
+ // Issue #257:sleep 冲突/模式阶段的 LLM 输出预算(原硬编码 2048,实测不足)。
127
+ sleepMaxTokens: [256, 131072],
128
+ // Issue #258:总览(dream_summarize)输入条数硬上限(0 = 不设上限)。
129
+ dreamSummaryMaxInputs: [0, 100000]
106
130
  };
107
131
  // 浮点开关的闭区间(与 config.js 的 z.number().min().max() 对齐)。与整数开关
108
132
  // 分开:面板的整数控件要求 Number.isInteger,而余弦相似度阈值必须允许小数。
@@ -125,8 +149,13 @@ const FEATURE_FLAG_STRINGS = [
125
149
  // 留空 = 用当前默认模型。
126
150
  "entityExtractionProvider",
127
151
  "entityExtractionModel",
152
+ // Issue #258:总览(dream_summarize)专用路由,留空 = 用巩固模型。
153
+ "dreamSummaryProvider",
154
+ "dreamSummaryModel",
128
155
  "localEmbedModel",
129
- "ollamaModel"
156
+ "ollamaModel",
157
+ // Issue #239 第 4 项:高峰时段串("09:00-18:00",空串 = 关闭)。
158
+ "summarizePeakHours"
130
159
  ];
131
160
  // URL 字符串开关:trim 后必须为空或合法 http/https URL(new URL() 校验协议,
132
161
  // 拒绝其余协议——这是 SSRF 防线的一部分)。
@@ -143,8 +172,10 @@ const FEATURE_FLAG_ENUMS = {
143
172
  summarizeDedupeMode: ["off", "title", "vector"],
144
173
  // Issue #126:sleep 冲突阶段的动作集(conflict 默认 = 现状;full = 六分支)。
145
174
  sleepActionSet: ["conflict", "full"],
146
- // Issue #125:dream 候选集构造方式(window 默认 = 现状;hybrid 并入向量组)。
147
- dreamCandidateMode: ["window", "hybrid"]
175
+ // Issue #125:dream 候选集构造方式(window 默认 = 现状;hybrid 并入向量组)。
176
+ dreamCandidateMode: ["window", "hybrid"],
177
+ // 本地嵌入池化:auto = 按模型族判定(BGE → cls),可显式覆盖为 cls / mean。
178
+ localEmbedPooling: ["auto", "cls", "mean"]
148
179
  };
149
180
  const FEATURE_FLAG_STRING_MAX = 200;
150
181