@modusensus/dsh-mneme 0.8.0 → 0.8.2

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 (59) hide show
  1. package/README.md +15 -5
  2. package/lib/api-standalone.js +12 -0
  3. package/lib/api.js +52 -14
  4. package/lib/client.js +130 -8
  5. package/lib/config.js +8 -0
  6. package/lib/dream/decisions.js +55 -8
  7. package/lib/dream/sleep.js +79 -17
  8. package/lib/dream.js +61 -9
  9. package/lib/entities/extractor.js +2 -2
  10. package/lib/index.js +6 -0
  11. package/lib/lang.js +13 -2
  12. package/lib/local-embedder.js +9 -1
  13. package/lib/reranker.js +38 -8
  14. package/lib/scope.js +39 -4
  15. package/lib/service.js +121 -18
  16. package/lib/store.js +243 -40
  17. package/lib/summarize.js +2 -2
  18. package/lib/tools.js +64 -30
  19. package/lib/vector-index.js +2 -8
  20. package/lib/version-check.js +122 -0
  21. package/package.json +2 -1
  22. package/scripts/sync-test-badge.mjs +61 -0
  23. package/src/api-standalone.js +12 -0
  24. package/src/api.js +52 -14
  25. package/src/config.js +8 -0
  26. package/src/dream/decisions.js +55 -8
  27. package/src/dream/sleep.js +79 -17
  28. package/src/dream.js +61 -9
  29. package/src/entities/extractor.js +2 -2
  30. package/src/index.js +6 -0
  31. package/src/lang.js +13 -2
  32. package/src/local-embedder.js +9 -1
  33. package/src/reranker.js +38 -8
  34. package/src/scope.js +39 -4
  35. package/src/service.js +121 -18
  36. package/src/store.js +243 -40
  37. package/src/summarize.js +2 -2
  38. package/src/tools.js +64 -30
  39. package/src/vector-index.js +2 -8
  40. package/src/version-check.js +122 -0
  41. package/test/api.test.js +5 -0
  42. package/test/conflict-freeze.test.js +15 -5
  43. package/test/dream-archive-guardrail.test.js +116 -0
  44. package/test/dream-coverage-degrade.test.js +86 -0
  45. package/test/dream.test.js +15 -11
  46. package/test/inject-first-round-bm25.test.js +89 -0
  47. package/test/llm-message-source.test.js +177 -0
  48. package/test/reranker-single-logit.test.js +73 -0
  49. package/test/scope-candidates.test.js +239 -0
  50. package/test/scope-explicit.test.js +452 -0
  51. package/test/scope-storage.test.js +13 -5
  52. package/test/scope-strict.test.js +107 -41
  53. package/test/skip-invalid-restore.test.js +7 -5
  54. package/test/sleep-scheduler-rearm.test.js +89 -0
  55. package/test/sleep.test.js +2 -2
  56. package/test/standalone-api.test.js +34 -1
  57. package/test/store-vector-cache.test.js +98 -0
  58. package/test/tools-memory-get-schema.test.js +65 -0
  59. package/test/version-check.test.js +152 -0
package/lib/service.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import { createHash, randomUUID } from "node:crypto";
2
2
  import { TYPE_FILE } from "./mirror.js";
3
3
  import { updatedAtBounds } from "./store.js";
4
+ import { normalizeExplicitScope, scopeKeyOf } from "./scope.js";
4
5
  import { STR, langOf } from "./lang.js";
5
6
  import { computeHeat } from "./heat.js";
6
7
  import { evaluateMemoryQuality } from "./quality-filter.js";
@@ -40,12 +41,8 @@ const CONTENT_HISTORY_MAX = 20;
40
41
  // 清单必须与 quality-filter.js 的写入端对齐(6 个,含 type 自指的 self_referential)。
41
42
  const SIGNAL_TAGS = ["low_quality", "duplicate", "meta", "repetitive", "short_content", "self_referential"];
42
43
 
43
- // v0.8.0 A1(issue #17)去重键的 scope 归一:非空 trim 字符串原样比较,其余
44
- // (NULL/undefined/空串)统一为「未标注」。与 store.normalizeScopeText 的落库
45
- // 口径一致——两把钥匙只有都过这里再比较,NULL 与 'global' 才不会错配。
46
- function scopeKeyOf(v) {
47
- return typeof v === "string" && v.trim() ? v.trim() : null;
48
- }
44
+ // scopeKeyOf 已上提到 ./scope.js(v0.8.1,issue #170 第 2 步):与 sleep 的跨
45
+ // scope 配对共用同一把比较钥匙,避免两处定义漂移。
49
46
 
50
47
  // v0.8.0 A2(issue #17)scope 检索加权系数:两维都无法确立 foreign 的候选
51
48
  // (命中行、未标注行、当前侧维度解析不到的行)→ 加成;只有确立了 foreign
@@ -93,10 +90,18 @@ function inOccurredBounds(m, bounds) {
93
90
  * 留给后续批次)。store.list 的 SQL 过滤与此谓词同口径(见 store.js visibility)。
94
91
  */
95
92
  function isVisibleInScope(m, current) {
96
- const agent = scopeKeyOf(m.agent_scope);
97
- if (agent !== null && (current.agent_scope === null || agent !== current.agent_scope)) return false;
98
- const ws = scopeKeyOf(m.workspace_scope);
99
- if (ws !== null && (current.workspace_scope === null || ws !== current.workspace_scope)) return false;
93
+ // v0.8.1 第 3 步(issue #170 4.3,作者已确认):硬过滤只认显式声明。某维
94
+ // 仅当「该维有标注且来源为 explicit」才构成硬墙;auto(含 v0.8.0 存量的
95
+ // NULL 来源行)只吃 A2 软加权(foreign ×0.5 保留可见),不进硬过滤。
96
+ // 显式全局(值 NULL + source explicit)与未标注同形,天然恒可见。
97
+ if (m.agent_scope_source === "explicit") {
98
+ const agent = scopeKeyOf(m.agent_scope);
99
+ if (agent !== null && (current.agent_scope === null || agent !== current.agent_scope)) return false;
100
+ }
101
+ if (m.workspace_scope_source === "explicit") {
102
+ const ws = scopeKeyOf(m.workspace_scope);
103
+ if (ws !== null && (current.workspace_scope === null || ws !== current.workspace_scope)) return false;
104
+ }
100
105
  return true;
101
106
  }
102
107
 
@@ -1064,17 +1069,19 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1064
1069
  // 标题变化,若仍按 (type, title) 匹配会出现两个活跃的 summary 同时注入。
1065
1070
  // 其余类型维持 (type, title) 匹配不变。
1066
1071
  const candidates = store.list({ type: memory.type, limit: 100 });
1067
- // v0.8.0 A1(issue #17):scopeEnabled 开启时去重键由 (type, title) 扩展为
1072
+ // v0.8.0 A1(issue #17):去重键由 (type, title) 扩展为
1068
1073
  // (type, title, agent_scope, workspace_scope, sensitivity)——不同作用域的同
1069
1074
  // 标题记忆绝不互相物理合并(旧逻辑跨 agent/workspace 并行,事后无法拆分)。
1070
1075
  // NULL 归一为「未标注」:未标注行之间互相匹配(含存量行),与已标注行不
1071
- // 匹配。flag 关闭时跳过比较,行为与 A1 前逐字节一致。
1072
- const scopeOn = config.scopeEnabled === true;
1076
+ // 匹配。
1077
+ // v0.8.1(issue #170 复核项 1):比较不再受 scopeEnabled 门控——显式声明
1078
+ // flag 关也生效(工具层明写「读了自动标注也生效」,去重必须同口径),否则
1079
+ // 关自动标注后同标题不同 scope 的行会物理合并进第一行的归属。存量全 NULL
1080
+ // 行互相匹配,A1 前行为不变。
1073
1081
  const scopeMatches = (m) =>
1074
- !scopeOn ||
1075
- (scopeKeyOf(m.agent_scope) === scopeKeyOf(memory.agent_scope) &&
1076
- scopeKeyOf(m.workspace_scope) === scopeKeyOf(memory.workspace_scope) &&
1077
- scopeKeyOf(m.sensitivity) === scopeKeyOf(memory.sensitivity));
1082
+ scopeKeyOf(m.agent_scope) === scopeKeyOf(memory.agent_scope) &&
1083
+ scopeKeyOf(m.workspace_scope) === scopeKeyOf(memory.workspace_scope) &&
1084
+ scopeKeyOf(m.sensitivity) === scopeKeyOf(memory.sensitivity);
1078
1085
  // Issue #127:写入端语义去重命中时,调用方用 _mergeInto 指定并入目标——复用
1079
1086
  // 下面这段并入逻辑(appendContent + content_history + 质量处置),不另写第二份
1080
1087
  // 实现。目标已被并发删除时回落到常规匹配。显式目标同样过 scope 门:跨作用域
@@ -1097,6 +1104,17 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1097
1104
  ? newContent
1098
1105
  : appendContent(existing.content, newContent);
1099
1106
  const importance = Math.min(5, Math.max(existing.importance, memory.importance ?? existing.importance));
1107
+ // v0.8.1 底座(issue #170):去重命中=scope 值完全同键,此时显式写入把
1108
+ // 被并入行的同维来源升级为 explicit(用户刚刚显式声明了同一归属),
1109
+ // 并刷新 scope_decided_at。仅升级来源、不改值——值改变会走新行。
1110
+ const scopeUpgrade = {};
1111
+ if (memory.agent_scope_source === "explicit" && existing.agent_scope_source !== "explicit") {
1112
+ scopeUpgrade.agent_scope_source = "explicit";
1113
+ }
1114
+ if (memory.workspace_scope_source === "explicit" && existing.workspace_scope_source !== "explicit") {
1115
+ scopeUpgrade.workspace_scope_source = "explicit";
1116
+ }
1117
+ if (Object.keys(scopeUpgrade).length) scopeUpgrade.scope_decided_at = new Date().toISOString();
1100
1118
  const merged = store.update(existing.id, {
1101
1119
  content,
1102
1120
  importance,
@@ -1105,8 +1123,30 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1105
1123
  content_history: pushContentHistory(existing, direct
1106
1124
  ? (memory._humanEdited === true ? "human_override" : "overwrite")
1107
1125
  : "auto_merge"),
1126
+ ...scopeUpgrade,
1108
1127
  ...(quality ? { quality_score: quality.score } : {})
1109
1128
  });
1129
+ // v0.8.1(issue #170 review 3):来源升级也是归属性质改变——与
1130
+ // updateMemory 的显式修正同等待遇,落一行 scope_changes(actor=tool:
1131
+ // 只有 memory_save 的显式参数会带 explicit 来源走到这里)。审计失败只
1132
+ // warn 不反噬合并。升级只在来源首次变化时触发,不会每次并入都写。
1133
+ if (Object.keys(scopeUpgrade).length) {
1134
+ try {
1135
+ store.saveScopeChange({
1136
+ memory_id: existing.id,
1137
+ actor: "tool",
1138
+ prev_agent_scope: existing.agent_scope ?? null,
1139
+ prev_workspace_scope: existing.workspace_scope ?? null,
1140
+ next_agent_scope: merged.agent_scope ?? null,
1141
+ next_workspace_scope: merged.workspace_scope ?? null,
1142
+ agent_scope_source: merged.agent_scope_source ?? null,
1143
+ workspace_scope_source: merged.workspace_scope_source ?? null,
1144
+ decided_at: scopeUpgrade.scope_decided_at
1145
+ });
1146
+ } catch (e) {
1147
+ try { logger?.warn?.(`[dsh-mneme] scope upgrade audit failed: ${String(e)}`); } catch { /* 不反噬 */ }
1148
+ }
1149
+ }
1110
1150
  // Bug7: a degraded/archived result is applied on top of the merged row.
1111
1151
  const result = applyQualityDisposition(merged, quality, qf);
1112
1152
  afterSync("write");
@@ -1122,8 +1162,12 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1122
1162
  importance: memory.importance ?? 3,
1123
1163
  source: memory.source ?? "manual",
1124
1164
  // v0.8.0 A1:scope 标注透传(store 端归一化,未标注落 NULL)。
1165
+ // v0.8.1 底座:来源(auto/explicit)与决策时间随行透传。
1125
1166
  agent_scope: memory.agent_scope,
1126
1167
  workspace_scope: memory.workspace_scope,
1168
+ agent_scope_source: memory.agent_scope_source,
1169
+ workspace_scope_source: memory.workspace_scope_source,
1170
+ scope_decided_at: memory.scope_decided_at,
1127
1171
  sensitivity: memory.sensitivity,
1128
1172
  occurred_at: memory.occurred_at,
1129
1173
  ...(quality ? { quality_score: quality.score } : {})
@@ -1248,6 +1292,22 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1248
1292
  if (m && !m.archived && INJECT_TYPES.has(m.type) && !m.forgotten && codingGate(m)) semanticItems.push(m);
1249
1293
  }
1250
1294
  }
1295
+ // Issue #198:首轮(无向量、无缓存召回)的同步兜底——BM25 词法召回领位。
1296
+ // 注入渲染是同步的(宿主 systemPrompt 的 contexts 不支持异步 text),
1297
+ // 查询向量只能异步 prefetch 给下一轮,首轮必 miss;此时纯静态排序会让
1298
+ // 老的高重要性 summary/preference 占满槽位,当前话题的 decision/project
1299
+ // 进不来。查询文本本身就在手上,BM25 是纯进程内同步计算,词法相关性
1300
+ // 足以把当前话题顶到前排;门控与向量路径一致(含 importance 阈值)。
1301
+ // 语义向量命中时本分支不参与,行为不变。
1302
+ if (!semanticItems.length) {
1303
+ for (const hit of bm25Recall(q, maxItems * 2)) {
1304
+ if (hit && !hit.archived && INJECT_TYPES.has(hit.type) && !hit.forgotten &&
1305
+ codingGate(hit) &&
1306
+ (hit.type === "summary" || hit.type === "preference" || hit.importance >= threshold)) {
1307
+ semanticItems.push(hit);
1308
+ }
1309
+ }
1310
+ }
1251
1311
  if (semanticItems.length) {
1252
1312
  const seen = new Set();
1253
1313
  const merged = [];
@@ -1441,8 +1501,12 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1441
1501
  // v0.8.0 A2:scope 标注与事件发生时间透出——条件展开(未标注行不带键,
1442
1502
  // DTO 与 A2 前逐字节同形;带 undefined 键会被 in-process schema 校验
1443
1503
  // 判违规,JSON 序列化虽会丢弃但形状不稳定)。
1504
+ // v0.8.1 底座:scope 来源与决策时间随行透出(同样条件展开)。
1444
1505
  ...(m.agent_scope !== undefined ? { agent_scope: m.agent_scope } : {}),
1445
1506
  ...(m.workspace_scope !== undefined ? { workspace_scope: m.workspace_scope } : {}),
1507
+ ...(m.agent_scope_source !== undefined ? { agent_scope_source: m.agent_scope_source } : {}),
1508
+ ...(m.workspace_scope_source !== undefined ? { workspace_scope_source: m.workspace_scope_source } : {}),
1509
+ ...(m.scope_decided_at !== undefined ? { scope_decided_at: m.scope_decided_at } : {}),
1446
1510
  ...(m.sensitivity !== undefined ? { sensitivity: m.sensitivity } : {}),
1447
1511
  ...(m.occurred_at !== undefined ? { occurred_at: m.occurred_at } : {})
1448
1512
  }));
@@ -1734,10 +1798,47 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1734
1798
  // tags——任何带 tags 的更新都会抹掉这条唯一的审计线索(报告实测 150 条低分
1735
1799
  // 记忆里恰好缺的 2 条就是被 update 过的,并因此误判过归档来源)。把 existing
1736
1800
  // 的系统信号标签并集保留;用户自传的标签照常生效。
1737
- const patch = (p && Array.isArray(p.tags) && old && Array.isArray(old.tags))
1801
+ let patch = (p && Array.isArray(p.tags) && old && Array.isArray(old.tags))
1738
1802
  ? { ...p, tags: [...new Set([...p.tags, ...old.tags.filter((t) => SIGNAL_TAGS.includes(t))])] }
1739
1803
  : p;
1804
+ // v0.8.1 底座(issue #170):显式 scope 修正。patch 携带 agent_scope /
1805
+ // workspace_scope 键(undefined=该维不动)时归一化并盖 explicit 章 +
1806
+ // scope_decided_at,随后写 scope_changes 审计行(actor:tool=模型侧 /
1807
+ // panel=人工侧)。审计失败只 warn,不反噬主写入。
1808
+ const nextAgentScope = p && p.agent_scope !== undefined ? normalizeExplicitScope(p.agent_scope) : undefined;
1809
+ const nextWorkspaceScope = p && p.workspace_scope !== undefined ? normalizeExplicitScope(p.workspace_scope) : undefined;
1810
+ const scopeChanged = nextAgentScope !== undefined || nextWorkspaceScope !== undefined;
1811
+ if (scopeChanged) {
1812
+ const decidedAt = new Date().toISOString();
1813
+ patch = { ...patch };
1814
+ if (nextAgentScope !== undefined) {
1815
+ patch.agent_scope = nextAgentScope;
1816
+ patch.agent_scope_source = "explicit";
1817
+ }
1818
+ if (nextWorkspaceScope !== undefined) {
1819
+ patch.workspace_scope = nextWorkspaceScope;
1820
+ patch.workspace_scope_source = "explicit";
1821
+ }
1822
+ patch.scope_decided_at = decidedAt;
1823
+ }
1740
1824
  const updated = store.update(id, patch);
1825
+ if (scopeChanged && old && updated) {
1826
+ try {
1827
+ store.saveScopeChange({
1828
+ memory_id: id,
1829
+ actor: ctx.actor === "panel" ? "panel" : "tool",
1830
+ prev_agent_scope: old.agent_scope ?? null,
1831
+ prev_workspace_scope: old.workspace_scope ?? null,
1832
+ next_agent_scope: updated.agent_scope ?? null,
1833
+ next_workspace_scope: updated.workspace_scope ?? null,
1834
+ agent_scope_source: updated.agent_scope_source ?? null,
1835
+ workspace_scope_source: updated.workspace_scope_source ?? null,
1836
+ decided_at: patch.scope_decided_at
1837
+ });
1838
+ } catch (e) {
1839
+ try { ctx.logger?.warn?.(`[dsh-mneme] scope change audit failed: ${String(e)}`); } catch { /* 同样不反噬 */ }
1840
+ }
1841
+ }
1741
1842
  // Record a user correction when any meaningful field changed and the
1742
1843
  // reflection failure tracker is enabled. expected = what it became,
1743
1844
  // actual = what it was before; query (when provided) captures the
@@ -1843,6 +1944,8 @@ export function createService({ store, mirror, config, onWrite, logger }) {
1843
1944
  notifyWrite();
1844
1945
  },
1845
1946
  update: updateMemory,
1947
+ // v0.8.1 底座:scope 归属修正审计(单条记忆,新→旧)。
1948
+ listScopeChanges: (memoryId, opts) => store.listScopeChanges(memoryId, opts),
1846
1949
  // Compare-and-set update: applies the patch only when the row still carries
1847
1950
  // `expectedUpdatedAt`. Returns undefined on a miss (no write) so the caller
1848
1951
  // can re-read and retry — the primitive that prevents lost updates across