@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.
- package/README.md +15 -5
- package/lib/api-standalone.js +12 -0
- package/lib/api.js +52 -14
- package/lib/client.js +130 -8
- package/lib/config.js +8 -0
- package/lib/dream/decisions.js +55 -8
- package/lib/dream/sleep.js +79 -17
- package/lib/dream.js +61 -9
- package/lib/entities/extractor.js +2 -2
- package/lib/index.js +6 -0
- package/lib/lang.js +13 -2
- package/lib/local-embedder.js +9 -1
- package/lib/reranker.js +38 -8
- package/lib/scope.js +39 -4
- package/lib/service.js +121 -18
- package/lib/store.js +243 -40
- package/lib/summarize.js +2 -2
- package/lib/tools.js +64 -30
- package/lib/vector-index.js +2 -8
- package/lib/version-check.js +122 -0
- package/package.json +2 -1
- package/scripts/sync-test-badge.mjs +61 -0
- package/src/api-standalone.js +12 -0
- package/src/api.js +52 -14
- package/src/config.js +8 -0
- package/src/dream/decisions.js +55 -8
- package/src/dream/sleep.js +79 -17
- package/src/dream.js +61 -9
- package/src/entities/extractor.js +2 -2
- package/src/index.js +6 -0
- package/src/lang.js +13 -2
- package/src/local-embedder.js +9 -1
- package/src/reranker.js +38 -8
- package/src/scope.js +39 -4
- package/src/service.js +121 -18
- package/src/store.js +243 -40
- package/src/summarize.js +2 -2
- package/src/tools.js +64 -30
- package/src/vector-index.js +2 -8
- package/src/version-check.js +122 -0
- package/test/api.test.js +5 -0
- package/test/conflict-freeze.test.js +15 -5
- package/test/dream-archive-guardrail.test.js +116 -0
- package/test/dream-coverage-degrade.test.js +86 -0
- package/test/dream.test.js +15 -11
- package/test/inject-first-round-bm25.test.js +89 -0
- package/test/llm-message-source.test.js +177 -0
- package/test/reranker-single-logit.test.js +73 -0
- package/test/scope-candidates.test.js +239 -0
- package/test/scope-explicit.test.js +452 -0
- package/test/scope-storage.test.js +13 -5
- package/test/scope-strict.test.js +107 -41
- package/test/skip-invalid-restore.test.js +7 -5
- package/test/sleep-scheduler-rearm.test.js +89 -0
- package/test/sleep.test.js +2 -2
- package/test/standalone-api.test.js +34 -1
- package/test/store-vector-cache.test.js +98 -0
- package/test/tools-memory-get-schema.test.js +65 -0
- 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.
|
|
44
|
-
//
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
-
// 匹配。
|
|
1072
|
-
|
|
1076
|
+
// 匹配。
|
|
1077
|
+
// v0.8.1(issue #170 复核项 1):比较不再受 scopeEnabled 门控——显式声明
|
|
1078
|
+
// flag 关也生效(工具层明写「读了自动标注也生效」,去重必须同口径),否则
|
|
1079
|
+
// 关自动标注后同标题不同 scope 的行会物理合并进第一行的归属。存量全 NULL
|
|
1080
|
+
// 行互相匹配,A1 前行为不变。
|
|
1073
1081
|
const scopeMatches = (m) =>
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
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
|
-
|
|
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
|