@modusensus/dsh-mneme 0.10.1 → 0.12.0

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 (97) hide show
  1. package/README.md +17 -6
  2. package/bin/dsh-mneme-choro.mjs +254 -0
  3. package/docs/CONFIGURATION.md +10 -1
  4. package/lib/api-standalone.js +17 -21
  5. package/lib/api.js +414 -12
  6. package/lib/bootstrap-http.js +105 -0
  7. package/lib/bootstrap.js +5 -1
  8. package/lib/choro-command.js +104 -0
  9. package/lib/client.js +1637 -110
  10. package/lib/config-keys.js +144 -0
  11. package/lib/config.js +60 -1
  12. package/lib/document.js +9 -3
  13. package/lib/dream/clustering.js +6 -1
  14. package/lib/index.js +87 -5
  15. package/lib/inject.js +18 -4
  16. package/lib/lang.js +82 -0
  17. package/lib/profile-facts.js +356 -0
  18. package/lib/quality-filter.js +48 -12
  19. package/lib/service.js +112 -4
  20. package/lib/settings.js +13 -1
  21. package/lib/store.js +978 -27
  22. package/lib/tools.js +244 -1
  23. package/lib/user-settings.js +63 -7
  24. package/lib/wiki/inject.js +164 -0
  25. package/lib/wiki/partition.js +200 -0
  26. package/lib/wiki/plan.js +214 -0
  27. package/lib/wiki/queries.js +360 -0
  28. package/lib/wiki/render.js +187 -0
  29. package/lib/wiki/scan.js +371 -0
  30. package/lib/wiki/t0.js +463 -0
  31. package/lib/wiki/t1.js +496 -0
  32. package/lib/write-admission.js +5 -3
  33. package/package.json +4 -2
  34. package/scripts/sync-test-badge.mjs +1 -1
  35. package/scripts/test-count-sync.mjs +9 -5
  36. package/src/api-standalone.js +17 -21
  37. package/src/api.js +414 -12
  38. package/src/bootstrap-http.js +105 -0
  39. package/src/bootstrap.js +5 -1
  40. package/src/choro-command.js +104 -0
  41. package/src/config-keys.js +144 -0
  42. package/src/config.js +60 -1
  43. package/src/document.js +9 -3
  44. package/src/dream/clustering.js +6 -1
  45. package/src/index.js +87 -5
  46. package/src/inject.js +18 -4
  47. package/src/lang.js +82 -0
  48. package/src/profile-facts.js +356 -0
  49. package/src/quality-filter.js +48 -12
  50. package/src/service.js +112 -4
  51. package/src/settings.js +13 -1
  52. package/src/store.js +978 -27
  53. package/src/tools.js +244 -1
  54. package/src/user-settings.js +63 -7
  55. package/src/wiki/inject.js +164 -0
  56. package/src/wiki/partition.js +200 -0
  57. package/src/wiki/plan.js +214 -0
  58. package/src/wiki/queries.js +360 -0
  59. package/src/wiki/render.js +187 -0
  60. package/src/wiki/scan.js +371 -0
  61. package/src/wiki/t0.js +463 -0
  62. package/src/wiki/t1.js +496 -0
  63. package/src/write-admission.js +5 -3
  64. package/test/api-routes-gaps.test.js +131 -1
  65. package/test/api.test.js +9 -1
  66. package/test/bootstrap.test.js +8 -0
  67. package/test/choro-command.test.js +218 -0
  68. package/test/client.test.js +802 -21
  69. package/test/config-keys-routes.test.js +233 -0
  70. package/test/config.test.js +79 -0
  71. package/test/contrast.test.js +14 -7
  72. package/test/document.test.js +32 -0
  73. package/test/embedding-cache-f32.test.js +276 -0
  74. package/test/inject-pin-explicit.test.js +300 -0
  75. package/test/inject-pin-vector-crowd.test.js +225 -0
  76. package/test/inject.test.js +51 -0
  77. package/test/llm-audit.test.js +29 -1
  78. package/test/persona-settings.test.js +31 -0
  79. package/test/profile-facts.test.js +492 -0
  80. package/test/quality-backfill.test.js +290 -0
  81. package/test/quality-filter.test.js +113 -4
  82. package/test/service.test.js +1 -1
  83. package/test/session-gate.test.js +1 -1
  84. package/test/test-count-sync.test.js +14 -3
  85. package/test/tool-state-ignore.test.js +65 -0
  86. package/test/tools-dto-schema-matrix.test.js +23 -2
  87. package/test/tools.test.js +144 -7
  88. package/test/wiki-api.test.js +273 -0
  89. package/test/wiki-inject.test.js +265 -0
  90. package/test/wiki-partition.test.js +112 -0
  91. package/test/wiki-plan.test.js +80 -0
  92. package/test/wiki-queries.test.js +270 -0
  93. package/test/wiki-scan.test.js +208 -0
  94. package/test/wiki-store.test.js +190 -0
  95. package/test/wiki-t0.test.js +380 -0
  96. package/test/wiki-t1.test.js +235 -0
  97. package/test/wiki-tools.test.js +176 -0
package/src/service.js CHANGED
@@ -516,9 +516,12 @@ export function createService({ store, mirror, config, onWrite, logger, document
516
516
  // Weighted blend factor for hybrid search; exposed so callers can tune it.
517
517
  const DEFAULT_HYBRID_WEIGHTS = { vector: 0.6, keyword: 0.4 };
518
518
 
519
- // Cosine over two plain arrays (shared by the search-time semantic dedup).
519
+ // Cosine over two numeric vectors (shared by the search-time semantic dedup).
520
+ // Accepts typed arrays too — the store's cache holds Float32Array, and
521
+ // `Array.isArray` is false for typed arrays (would score everything 0).
520
522
  function cosineVec(a, b) {
521
- if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length || !a.length) return 0;
523
+ const isVec = (v) => Array.isArray(v) || (ArrayBuffer.isView(v) && !(v instanceof DataView));
524
+ if (!isVec(a) || !isVec(b) || a.length !== b.length || !a.length) return 0;
522
525
  let dot = 0, na = 0, nb = 0;
523
526
  for (let i = 0; i < a.length; i++) {
524
527
  dot += a[i] * b[i];
@@ -1849,6 +1852,76 @@ export function createService({ store, mirror, config, onWrite, logger, document
1849
1852
  candidates = merged;
1850
1853
  }
1851
1854
  }
1855
+ // pin 池取数修复(#249 第一批):pin 类行被混合合并的封顶挡在候选池外面。
1856
+ // `merged.length >= poolSize` 在 pin 筛选**之前**就封顶(生产 maxItems=5 →
1857
+ // poolSize=10,向量路一次正好返回 10 条即 break),规则窗口里那些
1858
+ // constraint/preference 于是一条都进不了 candidates,pin 池只能从那 10 条里
1859
+ // 碰运气选——而本文件注释(:1919)与 issue #249 验收都写着「pin 不占
1860
+ // maxItems 名额」。实测真库 maxItems=5 时 pin shown 只有 2/5,补回后恒 5/5;
1861
+ // maxItems=10(poolSize=20)时前后都是 5/5——所以这不是多给名额,只是把被
1862
+ // 向量路吃掉的那部分补回来。
1863
+ // 两条纪律:①只在 pin 预算 > 0 时补,预算关时 candidates 与改前逐字节一致;
1864
+ // ②只补到候选池尾部,后面的 strictScope(:1923)与 scopeState(:1930)两道
1865
+ // 闸门照旧作用其上,「pin 池 eligible 取自过滤之后的候选」这条位置纪律不变。
1866
+ //
1867
+ // #249 第二批(显式 pin):判据从「类型」换成「用户亲手钉的 id」。两种口径
1868
+ // 由 pinned_memories 表**里能用的行**回答,而不是由「表里有几行」回答:
1869
+ // - 有可用行 → explicit:按 id 点查(store.listByIdsForInject)取回行,于是
1870
+ // **200 行规则窗口之外的记忆也能进**(这是第二层存在的理由本身);不再按
1871
+ // type 认 pin(写得最全的那版往往是 pattern,限类型就只能拿残缺版)。
1872
+ // - 一条可用行都没有 → 回落旧的类型口径,升级即等价;她钉第一条才切换。
1873
+ //
1874
+ // ⚠ 判据为什么必须看「能用的」而不是 countPinned()>0:pin 行会在 memories
1875
+ // 那边被归档、遗忘、硬删,而 pinned_memories 对这三种一无所知。用 count(*)
1876
+ // 当判据,只要表里剩一行(哪怕是归档行),pinMode 就永远钉在 explicit,而
1877
+ // eligible 恒为空 → pin 全灭且**永不回落**,无日志无报错——正是这批改动要
1878
+ // 治的那类静默失败。删除路径补 DELETE 只堵「硬删」这一种,归档/遗忘比硬删
1879
+ // 常见得多,所以根因在判据,不在清理。
1880
+ // 只要还有一条能用就留在 explicit、只注入能用的那几条:若因为「有 3 条归档」
1881
+ // 就整段回落到类型口径,会突然冒出一批她没钉过的条目,那比少注入更糟。
1882
+ // 补位仍在这段:早于 topic 重排(:1930)、strictScope(:1972)、scopeState
1883
+ // (:1979),所以越权行与暂停作用域行照样过两道闸——pin 不是绕过闸门的通道。
1884
+ // 预算 0 时整段不执行:不读表、不改 candidates,逐字节等于改前。
1885
+ const pinnedBudget = Math.max(0, Math.min(5, Math.floor(config?.pinnedInjectBudget ?? 0)));
1886
+ // null | "explicit" | "type-fallback" | "fallback-none-usable"
1887
+ let pinMode = null;
1888
+ let pinRows = []; // explicit:按 pin 先后(created_at ASC)排好、已滤遗忘/归档
1889
+ let pinOutsideWindow = 0; // explicit:点查取回、原本不在候选池的行数(观测点)
1890
+ if (pinnedBudget > 0) {
1891
+ let pinRecords = [];
1892
+ try {
1893
+ // 先读表(本表行数以个位计);能用的行下一步按 id 点查回来再判。
1894
+ pinRecords = store.countPinned() > 0 ? store.listPinned() : [];
1895
+ } catch { pinRecords = []; }
1896
+ if (pinRecords.length > 0) {
1897
+ try {
1898
+ // 点查带列裁剪(listByIdsForInject 复用 memoryColumnList,已排除 embedding)。
1899
+ // 遗忘/归档行在这里就滤掉:pin 不是复活通道(验收第 7 条)。
1900
+ const byId = new Map(store.listByIdsForInject(pinRecords.map((p) => p.memory_id))
1901
+ .map((m) => [m.id, m]));
1902
+ pinRows = pinRecords.map((p) => byId.get(p.memory_id)).filter((m) => m && !m.forgotten && !m.archived);
1903
+ } catch { pinRows = []; }
1904
+ }
1905
+ if (pinRows.length > 0) {
1906
+ pinMode = "explicit";
1907
+ const knownIds = new Set(candidates.map((m) => m.id));
1908
+ const additions = pinRows.filter((m) => !knownIds.has(m.id));
1909
+ pinOutsideWindow = additions.length;
1910
+ if (additions.length) candidates = [...candidates, ...additions];
1911
+ } else {
1912
+ // 表空,或表里有行但一条都用不了(归档/遗忘/被删)。两种都回落类型口径,
1913
+ // 但 source 分开记:后者是一次**静默降级**,必须留痕,否则读的人只会看到
1914
+ // 「pin 一条都没注入」而不知道为什么。
1915
+ pinMode = pinRecords.length > 0 ? "fallback-none-usable" : "type-fallback";
1916
+ if (candidates !== items) {
1917
+ const knownIds = new Set(candidates.map((m) => m.id));
1918
+ const spilledPins = items.filter(
1919
+ (m) => PINNED_MEMORY_TYPES.has(m.type) && m.graphHint !== true && !knownIds.has(m.id)
1920
+ );
1921
+ if (spilledPins.length) candidates = [...candidates, ...spilledPins];
1922
+ }
1923
+ }
1924
+ }
1852
1925
  // Topic-ranked selection (v0.5.0 2.2): when the current query's vector is
1853
1926
  // available the whole candidate list is re-ordered by similarity to that
1854
1927
  // vector, so the injected slots go to memories on the current topic
@@ -1909,12 +1982,24 @@ export function createService({ store, mirror, config, onWrite, logger, document
1909
1982
  // (验收:pin 不参与跨轮轮换)。独立预算的两层意义:pin 既不占 maxItems
1910
1983
  // 名额、也不被 document 预算截断,因此不会把当前任务需要的情景候选挤出去;
1911
1984
  // 超预算的条数回报给调用方,在块内如实标注(绝不静默省略)。
1912
- const pinnedBudget = Math.max(0, Math.min(5, Math.floor(config?.pinnedInjectBudget ?? 0)));
1985
+ // (pinnedBudget 与口径在候选池那段已算好——见 :1876 起的 #249 第二批注释。)
1913
1986
  // eligible 留到块外:未展示条数要等 general 槽选完才算得准(见 selected 之后)。
1914
1987
  // graphHint 行不进 pin 池:pin 是「每轮必进的约束/偏好」,而线索行按定义是
1915
1988
  // 「链路信息、非事实断言」,且 pin 长在候选池遍历之前——不排除的话,保守档
1916
1989
  // (graphInjectHint 关)也能从 pin 侧把它放回注入块,线索开关就被绕过去了。
1917
- const eligible = pinnedBudget > 0 ? candidates.filter((m) => PINNED_MEMORY_TYPES.has(m.type) && m.graphHint !== true) : [];
1990
+ //
1991
+ // 两种口径(#249 第二批):explicit 时 eligible 就是「钉过、点查取回、且过了
1992
+ // 两道闸(仍在 candidates 里)」的行,次序按 pin 先后(created_at ASC)——
1993
+ // 稳定偏好按「什么时候被钉」排,而不是按「最近改过」,正是第二层要修的
1994
+ // 那个排序维度;type-fallback 时保持旧口径(候选里按类型筛),升级即等价。
1995
+ const eligible = pinnedBudget > 0
1996
+ ? (pinMode === "explicit"
1997
+ ? (() => {
1998
+ const live = new Set(candidates.map((m) => m.id));
1999
+ return pinRows.filter((m) => live.has(m.id) && m.graphHint !== true);
2000
+ })()
2001
+ : candidates.filter((m) => PINNED_MEMORY_TYPES.has(m.type) && m.graphHint !== true))
2002
+ : [];
1918
2003
  let pinned = [];
1919
2004
  if (pinnedBudget > 0 && eligible.length > 0) {
1920
2005
  pinned = eligible.slice(0, pinnedBudget);
@@ -1969,6 +2054,17 @@ export function createService({ store, mirror, config, onWrite, logger, document
1969
2054
  const shownIds = new Set(selected.map((m) => m.id));
1970
2055
  pinnedStats.shown = pinned.length;
1971
2056
  pinnedStats.suppressed = eligible.filter((m) => !shownIds.has(m.id)).length;
2057
+ // #249 第二批:口径来源与「绕开窗口」的观测点。source 四档(预算 0 时整个
2058
+ // 键不出现):
2059
+ // "explicit" 用户钉的行里有能用的,按 pin 先后注入
2060
+ // "type-fallback" 表里从没钉过(含升级后还没钉),走旧类型口径
2061
+ // "fallback-none-usable" 钉过,但那些行全被归档/遗忘/删了 —— 这次是一次
2062
+ // 静默降级,留这一档就是为了让它不再静默
2063
+ // 前两档的行为差异在「窗口外也能进」:只有 explicit 享受点查补位,
2064
+ // outsideWindow 也只数这批(cognee 那条「别让它进候选池」的等价做法);
2065
+ // 两个 fallback 档恒 0(它们只能从窗口里取)。
2066
+ pinnedStats.source = pinMode ?? undefined;
2067
+ pinnedStats.outsideWindow = pinOutsideWindow;
1972
2068
  }
1973
2069
  // Issue #380:注入前判定(preInjectGate)在这里消费——**必须在 touchRecalled
1974
2070
  // 之前**,与 strictScope 同纪律(见本文件 :419-422 的注释:闸门放在记账之后,
@@ -2688,6 +2784,15 @@ export function createService({ store, mirror, config, onWrite, logger, document
2688
2784
  setScopeState: (r) => store.setScopeState(r),
2689
2785
  getScopeState: (d, v) => store.getScopeState(d, v),
2690
2786
  listScopeStates: (o) => store.listScopeStates(o),
2787
+ // #249 第二批:pin 的读写口。这四个都是纯 bookkeeping——它们不动任何记忆行,
2788
+ // 只动 pinned_memories 这张关系表,所以照 setScopeState 那一批的纪律**不挂
2789
+ // write hook**(不 afterSync、不 notifyWrite):pin 一条不该让 mirror 重渲染,
2790
+ // 也不该把那行记忆的 heat/embedding 重新排队。注入侧只经 store 读本表
2791
+ // (listPinned / listByIdsForInject),写侧只经这里,两处都绕开 memories 主表。
2792
+ pinMemory: (id, opts) => store.pinMemory(id, opts),
2793
+ unpinMemory: (id) => store.unpinMemory(id),
2794
+ listPinned: () => store.listPinned(),
2795
+ countPinned: () => store.countPinned(),
2691
2796
  // 巩固(dream.js)侧的过滤出口。暴露出来而不是让 dream 自己读状态表,是因为
2692
2797
  // 判定只能有一份实现(gateByScope 注释里那条「过滤点漂移」的教训);dream 侧
2693
2798
  // 的三个读入口各自的附加条件不同(有的排 forgotten、有的不排),但「被按下的
@@ -2922,6 +3027,8 @@ export function createService({ store, mirror, config, onWrite, logger, document
2922
3027
  listLlmAudits: (opts) => store.listLlmAudits(opts),
2923
3028
  countLlmAudits: (opts) => store.countLlmAudits(opts),
2924
3029
  getLlmAuditStats: (opts) => store.getLlmAuditStats(opts),
3030
+ // 面板「8 周沉淀」图(只读聚合):进来的 / 真沉淀的 / 归档的按天三条线。
3031
+ getConsolidationStats: (opts) => store.getConsolidationStats(opts),
2925
3032
  deleteOldLlmAudits: (before) => store.deleteOldLlmAudits(before),
2926
3033
  // Entity gene (v0.3.0) passthroughs for the autoDream apply path
2927
3034
  // (applyDecisions): records supersedes relations after an update and
@@ -2929,6 +3036,7 @@ export function createService({ store, mirror, config, onWrite, logger, document
2929
3036
  // passthroughs above — never write-hook-triggering memory mutations.
2930
3037
  saveRelation: (r) => store.saveRelation(r),
2931
3038
  listEntities: (o) => store.listEntities(o),
3039
+ countEntities: () => store.countEntities(),
2932
3040
  getRelations: (id) => store.getRelations(id),
2933
3041
  // #24 块4 被动确认的复核出口:列出权重被 bump 抬过的边(gain = 现权 −
2934
3042
  // 该 source 的初值)。纯读,不动权重——「看哪些边被用出来过」是人的动作。
package/src/settings.js CHANGED
@@ -192,6 +192,10 @@ const FEATURE_FLAG_BOOLEANS = [
192
192
  // 反向——那两个藏默认在场的工具,本键开默认不在场的能力(默认关=工具根本不存在,
193
193
  // 行为逐字节不变)。面板可启停=线上回滚开关。
194
194
  "organizeEnabled",
195
+ // chorography(仓库地志)的暴露闸:开默认不在场的三个只读工具 + 面板浏览页
196
+ // (默认关=模型工具列表里根本没有它们,行为逐字节不变)。同 organizeEnabled
197
+ // 的反向语义——见 config.js 的键注释。
198
+ "chorographyEnabled",
195
199
  // 嵌套对象开关:config.js 里是 memoryQualityFilter / llmAudit 对象的 enabled
196
200
  // 子字段。kv 按点号键平铺存("memoryQualityFilter.enabled": false),index.js
197
201
  // 合并时展开回嵌套对象,api.js 的 effective 从对象子字段取值。
@@ -243,6 +247,11 @@ const FEATURE_FLAG_BOOLEANS = [
243
247
  // Issue #497:画像提取草稿(POST /profile/extract)。默认关——每次调用一次 LLM 且
244
248
  // 读上百行记忆;产物只回草稿,绝不写 user_settings。进 LIGHT_MODE_OFF(预设关)。
245
249
  "profileExtractEnabled",
250
+ // 「Agent 眼中的你」自动事实(2026-11-06):dream 后逐卡维护 + 手动轮 + 注入段,
251
+ // 事实住 profile_facts 独立三表,绝不写 user_settings(#497 口径不变)。默认关,
252
+ // 进 LIGHT_MODE_OFF(自动维护轮与低资源档互斥)。顶层扁平键,与 config.js schema
253
+ // 成对出现(计数锁 test/api.test.js 钉住)。
254
+ "profileFactsEnabled",
246
255
  // #539:逐会话无痕默认关闭;子代理开关受总开关约束。手动 session id 名单留在配置,
247
256
  // 不进入只支持标量的 feature_flags 白名单。
248
257
  "sessionGateEnabled",
@@ -356,7 +365,10 @@ const FEATURE_FLAG_ENUMS = {
356
365
  // Issue #125:dream 候选集构造方式(window 默认 = 现状;hybrid 并入向量组)。
357
366
  dreamCandidateMode: ["window", "hybrid"],
358
367
  // 本地嵌入池化:auto = 按模型族判定(BGE → cls),可显式覆盖为 cls / mean。
359
- localEmbedPooling: ["auto", "cls", "mean"]
368
+ localEmbedPooling: ["auto", "cls", "mean"],
369
+ // M3 开局注入档位(spec §7):轻量档在消费侧压成 off,见 wiki/inject.js 的
370
+ // resolveInjectTier——LIGHT_MODE_OFF 对平铺键一律赋 false,枚举值进去会静默失效。
371
+ chorographyInject: ["off", "light", "full"]
360
372
  };
361
373
  const FEATURE_FLAG_STRING_MAX = 200;
362
374