@modusensus/dsh-mneme 0.7.32 → 0.8.1
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 +44 -20
- package/lib/api.js +100 -12
- package/lib/client.js +276 -4
- package/lib/commands.js +30 -3
- package/lib/config.js +21 -0
- package/lib/dream/sleep.js +43 -12
- package/lib/dream.js +43 -2
- package/lib/inject.js +39 -5
- package/lib/lang.js +23 -0
- package/lib/scope.js +118 -0
- package/lib/service.js +374 -62
- package/lib/settings.js +7 -0
- package/lib/store.js +245 -7
- package/lib/tools.js +138 -17
- package/lib/version-check.js +122 -0
- package/package.json +1 -1
- package/src/api.js +100 -12
- package/src/commands.js +30 -3
- package/src/config.js +21 -0
- package/src/dream/sleep.js +43 -12
- package/src/dream.js +43 -2
- package/src/inject.js +39 -5
- package/src/lang.js +23 -0
- package/src/scope.js +118 -0
- package/src/service.js +374 -62
- package/src/settings.js +7 -0
- package/src/store.js +245 -7
- package/src/tools.js +138 -17
- package/src/version-check.js +122 -0
- package/test/api.test.js +90 -4
- package/test/client.test.js +58 -0
- package/test/commands.test.js +40 -3
- package/test/conflict-freeze.test.js +15 -5
- package/test/inject.test.js +93 -0
- package/test/scope-candidates.test.js +239 -0
- package/test/scope-explicit.test.js +452 -0
- package/test/scope-retrieval.test.js +197 -0
- package/test/scope-storage.test.js +255 -0
- package/test/scope-strict.test.js +231 -0
- package/test/service.test.js +76 -0
- package/test/version-check.test.js +152 -0
package/src/service.js
CHANGED
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { createHash, randomUUID } from "node:crypto";
|
|
2
2
|
import { TYPE_FILE } from "./mirror.js";
|
|
3
|
+
import { updatedAtBounds } from "./store.js";
|
|
4
|
+
import { normalizeExplicitScope, scopeKeyOf } from "./scope.js";
|
|
3
5
|
import { STR, langOf } from "./lang.js";
|
|
4
6
|
import { computeHeat } from "./heat.js";
|
|
5
7
|
import { evaluateMemoryQuality } from "./quality-filter.js";
|
|
8
|
+
import { applyDecisions } from "./dream/decisions.js";
|
|
6
9
|
import { createBM25Index } from "./search/bm25.js";
|
|
7
10
|
import { adaptiveThreshold } from "./search/adaptive.js";
|
|
8
11
|
|
|
@@ -38,6 +41,70 @@ const CONTENT_HISTORY_MAX = 20;
|
|
|
38
41
|
// 清单必须与 quality-filter.js 的写入端对齐(6 个,含 type 自指的 self_referential)。
|
|
39
42
|
const SIGNAL_TAGS = ["low_quality", "duplicate", "meta", "repetitive", "short_content", "self_referential"];
|
|
40
43
|
|
|
44
|
+
// scopeKeyOf 已上提到 ./scope.js(v0.8.1,issue #170 第 2 步):与 sleep 的跨
|
|
45
|
+
// scope 配对共用同一把比较钥匙,避免两处定义漂移。
|
|
46
|
+
|
|
47
|
+
// v0.8.0 A2(issue #17)scope 检索加权系数:两维都无法确立 foreign 的候选
|
|
48
|
+
// (命中行、未标注行、当前侧维度解析不到的行)→ 加成;只有确立了 foreign
|
|
49
|
+
// (记忆带标注 + 当前维度可解析 + 值不等)→ 降权但保留可见(硬过滤是 A3
|
|
50
|
+
// strictScope)。即未标注行与命中行同列、不吃惩罚,但注意它们吃的是 BOOST
|
|
51
|
+
// 而非 ×1 中性。系数是模块常量而非配置项:A2 只定性行为,数值要等线上检索
|
|
52
|
+
// 质量反馈再调(加配置面=提前优化)。
|
|
53
|
+
const SCOPE_MATCH_BOOST = 1.25;
|
|
54
|
+
const SCOPE_FOREIGN_PENALTY = 0.5;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* 单条候选的 scope 乘数。匹配语义与去重键一致(scopeKeyOf):记忆侧未标注视同
|
|
58
|
+
* 该维度命中(全局记忆对谁都可见);当前会话侧某维度解析不到则该维度不参与
|
|
59
|
+
* 比较(无法比较 ≠ 不匹配)。
|
|
60
|
+
*/
|
|
61
|
+
function scopeMultiplier(m, current) {
|
|
62
|
+
const agentForeign = scopeKeyOf(m.agent_scope) !== null
|
|
63
|
+
&& current.agent_scope !== null
|
|
64
|
+
&& scopeKeyOf(m.agent_scope) !== current.agent_scope;
|
|
65
|
+
const workspaceForeign = scopeKeyOf(m.workspace_scope) !== null
|
|
66
|
+
&& current.workspace_scope !== null
|
|
67
|
+
&& scopeKeyOf(m.workspace_scope) !== current.workspace_scope;
|
|
68
|
+
return (agentForeign || workspaceForeign) ? SCOPE_FOREIGN_PENALTY : SCOPE_MATCH_BOOST;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* v0.8.0 A2:occurred_at 后置过滤谓词(搜索融合池用)。行侧取
|
|
73
|
+
* COALESCE(occurred_at, created_at)——与 store.list 的 SQL 过滤同口径,未标注
|
|
74
|
+
* 行回退创建时间;时间戳损坏的行保留(宁可放宽也不误杀,同 summarize inWindow)。
|
|
75
|
+
*/
|
|
76
|
+
function inOccurredBounds(m, bounds) {
|
|
77
|
+
const t = Date.parse(String(m.occurred_at ?? m.created_at ?? ""));
|
|
78
|
+
if (!Number.isFinite(t)) return true;
|
|
79
|
+
if (bounds.from && t < Date.parse(bounds.from)) return false;
|
|
80
|
+
if (bounds.to && t > Date.parse(bounds.to)) return false;
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* v0.8.0 A3(issue #17)strictScope 可见性谓词:issue 的四象限可见性公式
|
|
86
|
+
* (agent 不对称可见性)——记忆对当前会话可见 ⇔ (agent 维:未标注 或 命中当前
|
|
87
|
+
* agent) AND (workspace 维:未标注 或 命中当前 workspace)。NULL=未标注=全局;
|
|
88
|
+
* 当前会话某维度解析不到时,该维度带标注的记忆一律不可见(fail-closed:身份
|
|
89
|
+
* 不明的会话只见全局,不冒认)。sensitivity 是标签不参与可见性判定(其语义
|
|
90
|
+
* 留给后续批次)。store.list 的 SQL 过滤与此谓词同口径(见 store.js visibility)。
|
|
91
|
+
*/
|
|
92
|
+
function isVisibleInScope(m, current) {
|
|
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
|
+
}
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
41
108
|
/** Prepend the previous content to a memory's content_history (FIFO capped). */
|
|
42
109
|
function pushContentHistory(existing, source) {
|
|
43
110
|
const history = Array.isArray(existing?.content_history) ? existing.content_history : [];
|
|
@@ -589,7 +656,19 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
589
656
|
* a vector/rerank failure degrades to keyword results.
|
|
590
657
|
*/
|
|
591
658
|
async function searchMemories(query, options = {}) {
|
|
592
|
-
const {
|
|
659
|
+
const {
|
|
660
|
+
mode = "auto",
|
|
661
|
+
topK = 20,
|
|
662
|
+
threshold,
|
|
663
|
+
useRerank = true,
|
|
664
|
+
// v0.8.0 A2(issue #17):当前会话 scope(工具层从 exec 解析后传入)与
|
|
665
|
+
// occurred_at 时间窗。两者都只在显式传入时生效,既有调用方(注入/梦/评测)
|
|
666
|
+
// 不传 → 行为与 A2 前一致。
|
|
667
|
+
scope = null,
|
|
668
|
+
occurredFrom = null,
|
|
669
|
+
occurredTo = null,
|
|
670
|
+
recordRecall = options.recordRecall ?? (config?.recallRecordDefault ?? true)
|
|
671
|
+
} = options;
|
|
593
672
|
const q = String(query ?? "").trim();
|
|
594
673
|
if (!q) return [];
|
|
595
674
|
|
|
@@ -656,6 +735,19 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
656
735
|
|
|
657
736
|
const { merged: fusedMerged, signals } = fuseRecall({ keyword, vector, bm25, lim, mode, wv, wk, wb });
|
|
658
737
|
let merged = fusedMerged;
|
|
738
|
+
// v0.8.0 A3(issue #17):strictScope 硬过滤——他 scope 的候选直接出局
|
|
739
|
+
// (区别于 A2 的降权保留可见);未标注行与命中行保留。strict 与 A2 加权
|
|
740
|
+
// 叠加:过滤后剩下的命中行仍吃加成。scope 未传(flag 关)或完全解析不到
|
|
741
|
+
// 时跳过——identity 为空的对象({null,null})按 fail-closed 过滤。
|
|
742
|
+
if (config.strictScope === true && scope) {
|
|
743
|
+
merged = merged.filter((m) => isVisibleInScope(m, scope));
|
|
744
|
+
}
|
|
745
|
+
// v0.8.0 A2:occurred_at 时间过滤——在融合池上先滤再 dedup/slice,rerank
|
|
746
|
+
// 只看窗内候选,topK 槽位不被窗外行占用。
|
|
747
|
+
const occurredBounds = updatedAtBounds(occurredFrom, occurredTo);
|
|
748
|
+
if (occurredBounds.from || occurredBounds.to) {
|
|
749
|
+
merged = merged.filter((m) => inOccurredBounds(m, occurredBounds));
|
|
750
|
+
}
|
|
659
751
|
// Signal transparency (#2): decorate each returned row with its per-source
|
|
660
752
|
// scores and the final fused score. Purely additive — never changes rank.
|
|
661
753
|
if (config?.signalTransparency === true) {
|
|
@@ -685,6 +777,18 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
685
777
|
.slice(0, lim);
|
|
686
778
|
}
|
|
687
779
|
|
|
780
|
+
// v0.8.0 A2(issue #17):scope 检索加权——两维都无法确立 foreign 的候选
|
|
781
|
+
// (命中/未标注/当前侧解析不到)加成,确立 foreign 的候选降权但保留可见
|
|
782
|
+
// (硬过滤是 A3 strictScope);未标注行与命中行同列吃 BOOST、不被压制。
|
|
783
|
+
// flag 关或调用方未传 scope 时不动分,排序与 A2 前逐字节一致。与
|
|
784
|
+
// epistemic 加权同款收尾:乘分 → 降序 → 截 topK。
|
|
785
|
+
if (config.scopeEnabled === true && scope && (scope.agent_scope || scope.workspace_scope)) {
|
|
786
|
+
result = result
|
|
787
|
+
.map((m) => ({ ...m, score: (m.score ?? 0) * scopeMultiplier(m, scope) }))
|
|
788
|
+
.sort((a, b) => (b.score ?? 0) - (a.score ?? 0))
|
|
789
|
+
.slice(0, lim);
|
|
790
|
+
}
|
|
791
|
+
|
|
688
792
|
// Recall layer receipt: with recordRecall on, hand the actual merged
|
|
689
793
|
// candidate list (id/title/content/score/source) to the injected recorder
|
|
690
794
|
// before returning, making the retrieval scene replayable — the sibling of
|
|
@@ -965,15 +1069,30 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
965
1069
|
// 标题变化,若仍按 (type, title) 匹配会出现两个活跃的 summary 同时注入。
|
|
966
1070
|
// 其余类型维持 (type, title) 匹配不变。
|
|
967
1071
|
const candidates = store.list({ type: memory.type, limit: 100 });
|
|
1072
|
+
// v0.8.0 A1(issue #17):去重键由 (type, title) 扩展为
|
|
1073
|
+
// (type, title, agent_scope, workspace_scope, sensitivity)——不同作用域的同
|
|
1074
|
+
// 标题记忆绝不互相物理合并(旧逻辑跨 agent/workspace 并行,事后无法拆分)。
|
|
1075
|
+
// NULL 归一为「未标注」:未标注行之间互相匹配(含存量行),与已标注行不
|
|
1076
|
+
// 匹配。
|
|
1077
|
+
// v0.8.1(issue #170 复核项 1):比较不再受 scopeEnabled 门控——显式声明
|
|
1078
|
+
// flag 关也生效(工具层明写「读了自动标注也生效」,去重必须同口径),否则
|
|
1079
|
+
// 关自动标注后同标题不同 scope 的行会物理合并进第一行的归属。存量全 NULL
|
|
1080
|
+
// 行互相匹配,A1 前行为不变。
|
|
1081
|
+
const scopeMatches = (m) =>
|
|
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);
|
|
968
1085
|
// Issue #127:写入端语义去重命中时,调用方用 _mergeInto 指定并入目标——复用
|
|
969
1086
|
// 下面这段并入逻辑(appendContent + content_history + 质量处置),不另写第二份
|
|
970
|
-
//
|
|
971
|
-
|
|
972
|
-
|
|
1087
|
+
// 实现。目标已被并发删除时回落到常规匹配。显式目标同样过 scope 门:跨作用域
|
|
1088
|
+
// 的语义相似不并入(防泄漏),回落为独立新行。
|
|
1089
|
+
const explicitTarget = memory._mergeInto ? store.getById(memory._mergeInto) : undefined;
|
|
1090
|
+
const existing = explicitTarget
|
|
1091
|
+
? (scopeMatches(explicitTarget) ? explicitTarget : undefined)
|
|
973
1092
|
: (memory.type === "summary" && memory.source === "dream")
|
|
974
|
-
? (candidates.find((m) => m.source === "dream")
|
|
975
|
-
?? candidates.find((m) => m.title.trim() === String(memory.title).trim()))
|
|
976
|
-
: candidates.find((m) => m.title.trim() === String(memory.title).trim());
|
|
1093
|
+
? (candidates.find((m) => m.source === "dream" && scopeMatches(m))
|
|
1094
|
+
?? candidates.find((m) => scopeMatches(m) && m.title.trim() === String(memory.title).trim()))
|
|
1095
|
+
: candidates.find((m) => scopeMatches(m) && m.title.trim() === String(memory.title).trim());
|
|
977
1096
|
if (existing) {
|
|
978
1097
|
const newContent = String(memory.content ?? "");
|
|
979
1098
|
if (!newContent.trim()) {
|
|
@@ -985,6 +1104,17 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
985
1104
|
? newContent
|
|
986
1105
|
: appendContent(existing.content, newContent);
|
|
987
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();
|
|
988
1118
|
const merged = store.update(existing.id, {
|
|
989
1119
|
content,
|
|
990
1120
|
importance,
|
|
@@ -993,8 +1123,30 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
993
1123
|
content_history: pushContentHistory(existing, direct
|
|
994
1124
|
? (memory._humanEdited === true ? "human_override" : "overwrite")
|
|
995
1125
|
: "auto_merge"),
|
|
1126
|
+
...scopeUpgrade,
|
|
996
1127
|
...(quality ? { quality_score: quality.score } : {})
|
|
997
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
|
+
}
|
|
998
1150
|
// Bug7: a degraded/archived result is applied on top of the merged row.
|
|
999
1151
|
const result = applyQualityDisposition(merged, quality, qf);
|
|
1000
1152
|
afterSync("write");
|
|
@@ -1009,6 +1161,15 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1009
1161
|
tags: memory.tags ?? [],
|
|
1010
1162
|
importance: memory.importance ?? 3,
|
|
1011
1163
|
source: memory.source ?? "manual",
|
|
1164
|
+
// v0.8.0 A1:scope 标注透传(store 端归一化,未标注落 NULL)。
|
|
1165
|
+
// v0.8.1 底座:来源(auto/explicit)与决策时间随行透传。
|
|
1166
|
+
agent_scope: memory.agent_scope,
|
|
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,
|
|
1171
|
+
sensitivity: memory.sensitivity,
|
|
1172
|
+
occurred_at: memory.occurred_at,
|
|
1012
1173
|
...(quality ? { quality_score: quality.score } : {})
|
|
1013
1174
|
});
|
|
1014
1175
|
const result = applyQualityDisposition(created, quality, qf);
|
|
@@ -1074,7 +1235,7 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1074
1235
|
* fills + dedupes the remaining slots. Empty query / no cached recall /
|
|
1075
1236
|
* hybridInject off → pure legacy rule-based selection.
|
|
1076
1237
|
*/
|
|
1077
|
-
function injectCandidates({ query = "", maxItems = 5, threshold = 3, queryVector } = {}) {
|
|
1238
|
+
function injectCandidates({ query = "", maxItems = 5, threshold = 3, queryVector, scope = null } = {}) {
|
|
1078
1239
|
const q = String(query ?? "").trim();
|
|
1079
1240
|
// codingRetrospect 读取侧门控:编码记忆(rejected_solution / pitfall /
|
|
1080
1241
|
// constraint)只在编码任务时注入,防噪声污染其他业务;编码任务时按
|
|
@@ -1164,6 +1325,11 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1164
1325
|
}
|
|
1165
1326
|
} catch { /* topic re-rank unavailable: keep rule-based order */ }
|
|
1166
1327
|
}
|
|
1328
|
+
// v0.8.0 A3(issue #17):strictScope 硬过滤同样作用于自动注入——scoped 记忆
|
|
1329
|
+
// 泄进无关注入上下文是最典型的越权通道,检索侧过滤挡不住这里。
|
|
1330
|
+
if (config?.strictScope === true && scope) {
|
|
1331
|
+
candidates = candidates.filter((m) => isVisibleInScope(m, scope));
|
|
1332
|
+
}
|
|
1167
1333
|
const selected = candidates.slice(0, maxItems);
|
|
1168
1334
|
touchRecalled(selected);
|
|
1169
1335
|
return selected;
|
|
@@ -1217,6 +1383,94 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1217
1383
|
return applied;
|
|
1218
1384
|
}
|
|
1219
1385
|
|
|
1386
|
+
/**
|
|
1387
|
+
* v0.8.0 冲突集中处理(issue 反馈:冻结冲突没有人工出口——store 的
|
|
1388
|
+
* resolveConflictPending 此前无任何 API/UI 调用)。人工确认一条待审冲突:
|
|
1389
|
+
* 盖章 resolved_at/resolved_winner(审计),可选按 dream 非冻结 conflict
|
|
1390
|
+
* 路径**同款**处置落地——复用 applyDecisions 的 conflict 分支,CAS 快照 +
|
|
1391
|
+
* 事务 + 胜者追加已否决注记 + 败者归档 + 幂等全部同源,不另写第二份裁决。
|
|
1392
|
+
*
|
|
1393
|
+
* @param {string} id conflict_pending 行 id
|
|
1394
|
+
* @param {{winner?: "a"|"b"|null, apply?: boolean}} opts
|
|
1395
|
+
* winner: 选哪一方保留(a = memory_a,冻结时 LLM 的建议胜者;b = memory_b);
|
|
1396
|
+
* null = 仅盖章标记已读、不落地处置。人工选择是权威裁决:传入
|
|
1397
|
+
* applyDecisions 的 config 关掉 trustEpistemicWeighting——可信度自动换位
|
|
1398
|
+
* 只应作用于 LLM 提议对,不能推翻人工选择。
|
|
1399
|
+
* @returns {object|undefined} 盖章后的冲突行 + disposition 摘要;未知 id 返回 undefined
|
|
1400
|
+
*/
|
|
1401
|
+
function resolveConflictPending(id, { winner = null, apply = true } = {}) {
|
|
1402
|
+
const rows = store.listConflictPending({ includeResolved: true, limit: 500 });
|
|
1403
|
+
const row = rows.find((r) => r.id === id);
|
|
1404
|
+
if (!row) return undefined;
|
|
1405
|
+
const winnerId = winner === "a" ? row.memory_a : winner === "b" ? row.memory_b : null;
|
|
1406
|
+
const loserId = winner === "a" ? row.memory_b : winner === "b" ? row.memory_a : null;
|
|
1407
|
+
let disposition = null;
|
|
1408
|
+
if (apply && winnerId && loserId) {
|
|
1409
|
+
const snapshot = new Map();
|
|
1410
|
+
for (const m of [store.getById(row.memory_a), store.getById(row.memory_b)]) {
|
|
1411
|
+
if (m) snapshot.set(m.id, { updated_at: m.updated_at, content: m.content, title: m.title });
|
|
1412
|
+
}
|
|
1413
|
+
// applyDecisions 需要一个 service 形状的参数(getById/update/setArchived/
|
|
1414
|
+
// transaction)——委托给闭包内的同名实现,mirror/通知/嵌入与主写路径同源。
|
|
1415
|
+
const applyService = {
|
|
1416
|
+
getById: (mid) => store.getById(mid),
|
|
1417
|
+
update: (mid, patch) => updateMemory(mid, patch),
|
|
1418
|
+
setArchived: (mid, v) => archiveMemory(mid, v),
|
|
1419
|
+
transaction: (fn) => transaction(fn)
|
|
1420
|
+
};
|
|
1421
|
+
const { committed, failures } = applyDecisions(
|
|
1422
|
+
[{ action: "conflict", winner: winnerId, loser: loserId, reason: row.reason ?? "" }],
|
|
1423
|
+
applyService,
|
|
1424
|
+
logger,
|
|
1425
|
+
snapshot,
|
|
1426
|
+
{ ...config, trustEpistemicWeighting: false }
|
|
1427
|
+
);
|
|
1428
|
+
// Review 发现:处置必须**先于**盖章。原实现先 resolveConflictPending 盖章、
|
|
1429
|
+
// 再 applyDecisions——若 CAS 拒掉(确认前双方被并发更新),行已 resolved 从
|
|
1430
|
+
// 队列消失、前端仍显示成功,审计却记了"已确认"且无法重试。现在失败时不盖章
|
|
1431
|
+
// 直接返回:行留在队列待重试,审计不被污染。
|
|
1432
|
+
if (failures.length) {
|
|
1433
|
+
return { ...row, disposition: { ok: false, failures } };
|
|
1434
|
+
}
|
|
1435
|
+
disposition = { ok: true, committed };
|
|
1436
|
+
}
|
|
1437
|
+
// 处置成功(或纯标记)后才盖章。幂等重放(败者已归档 → skipped)也走这里:
|
|
1438
|
+
// 处置本就存在,标记已审即可。
|
|
1439
|
+
const stamped = store.resolveConflictPending(id, { winner: winnerId });
|
|
1440
|
+
if (!stamped) return undefined;
|
|
1441
|
+
return { ...stamped, disposition };
|
|
1442
|
+
}
|
|
1443
|
+
|
|
1444
|
+
/**
|
|
1445
|
+
* v0.8.0 冲突队列视图数据源:未解决冲突行,联表带出双方的当前状态(标题/
|
|
1446
|
+
* 内容/类型/重要性/归档标记),面板一处即可集中审阅,不必按徽章逐条找。
|
|
1447
|
+
* 参与记忆被删除/失联时该侧返回 {missing: true}(队列不因此丢行)。
|
|
1448
|
+
*/
|
|
1449
|
+
function listConflictQueue({ limit = 50, offset = 0 } = {}) {
|
|
1450
|
+
const rows = store.listConflictPending({ limit, offset, includeResolved: false });
|
|
1451
|
+
const side = (id) => {
|
|
1452
|
+
const m = store.getById(id);
|
|
1453
|
+
if (!m) return { id, missing: true };
|
|
1454
|
+
return {
|
|
1455
|
+
id,
|
|
1456
|
+
title: m.title,
|
|
1457
|
+
type: m.type,
|
|
1458
|
+
importance: m.importance,
|
|
1459
|
+
content: m.content,
|
|
1460
|
+
archived: m.archived === true,
|
|
1461
|
+
forgotten: m.forgotten === true
|
|
1462
|
+
};
|
|
1463
|
+
};
|
|
1464
|
+
return rows.map((r) => ({
|
|
1465
|
+
id: r.id,
|
|
1466
|
+
run_id: r.run_id ?? null,
|
|
1467
|
+
reason: r.reason ?? null,
|
|
1468
|
+
created_at: r.created_at,
|
|
1469
|
+
memory_a: side(r.memory_a),
|
|
1470
|
+
memory_b: side(r.memory_b)
|
|
1471
|
+
}));
|
|
1472
|
+
}
|
|
1473
|
+
|
|
1220
1474
|
function toApiList(rows) {
|
|
1221
1475
|
return rows.map((m) => ({
|
|
1222
1476
|
id: m.id,
|
|
@@ -1227,7 +1481,18 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1227
1481
|
importance: m.importance,
|
|
1228
1482
|
source: m.source,
|
|
1229
1483
|
created_at: m.created_at,
|
|
1230
|
-
updated_at: m.updated_at
|
|
1484
|
+
updated_at: m.updated_at,
|
|
1485
|
+
// v0.8.0 A2:scope 标注与事件发生时间透出——条件展开(未标注行不带键,
|
|
1486
|
+
// DTO 与 A2 前逐字节同形;带 undefined 键会被 in-process schema 校验
|
|
1487
|
+
// 判违规,JSON 序列化虽会丢弃但形状不稳定)。
|
|
1488
|
+
// v0.8.1 底座:scope 来源与决策时间随行透出(同样条件展开)。
|
|
1489
|
+
...(m.agent_scope !== undefined ? { agent_scope: m.agent_scope } : {}),
|
|
1490
|
+
...(m.workspace_scope !== undefined ? { workspace_scope: m.workspace_scope } : {}),
|
|
1491
|
+
...(m.agent_scope_source !== undefined ? { agent_scope_source: m.agent_scope_source } : {}),
|
|
1492
|
+
...(m.workspace_scope_source !== undefined ? { workspace_scope_source: m.workspace_scope_source } : {}),
|
|
1493
|
+
...(m.scope_decided_at !== undefined ? { scope_decided_at: m.scope_decided_at } : {}),
|
|
1494
|
+
...(m.sensitivity !== undefined ? { sensitivity: m.sensitivity } : {}),
|
|
1495
|
+
...(m.occurred_at !== undefined ? { occurred_at: m.occurred_at } : {})
|
|
1231
1496
|
}));
|
|
1232
1497
|
}
|
|
1233
1498
|
|
|
@@ -1508,6 +1773,99 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1508
1773
|
}
|
|
1509
1774
|
}
|
|
1510
1775
|
|
|
1776
|
+
// 返回字面量与内部裁决器(resolveConflictPending → applyDecisions 的 conflict
|
|
1777
|
+
// 处置)共用的写路径:mirror 同步、写通知、嵌入调度全部同源,不能各写一份。
|
|
1778
|
+
function updateMemory(id, p, ctx = {}) {
|
|
1779
|
+
const old = store.getById(id);
|
|
1780
|
+
// Issue #135 附属发现 2:质量过滤器把「为什么被降权/归档」写在系统信号标签
|
|
1781
|
+
// 上(SIGNAL_TAGS,applyQualityDisposition 以并集写入),而这里整组替换
|
|
1782
|
+
// tags——任何带 tags 的更新都会抹掉这条唯一的审计线索(报告实测 150 条低分
|
|
1783
|
+
// 记忆里恰好缺的 2 条就是被 update 过的,并因此误判过归档来源)。把 existing
|
|
1784
|
+
// 的系统信号标签并集保留;用户自传的标签照常生效。
|
|
1785
|
+
let patch = (p && Array.isArray(p.tags) && old && Array.isArray(old.tags))
|
|
1786
|
+
? { ...p, tags: [...new Set([...p.tags, ...old.tags.filter((t) => SIGNAL_TAGS.includes(t))])] }
|
|
1787
|
+
: p;
|
|
1788
|
+
// v0.8.1 底座(issue #170):显式 scope 修正。patch 携带 agent_scope /
|
|
1789
|
+
// workspace_scope 键(undefined=该维不动)时归一化并盖 explicit 章 +
|
|
1790
|
+
// scope_decided_at,随后写 scope_changes 审计行(actor:tool=模型侧 /
|
|
1791
|
+
// panel=人工侧)。审计失败只 warn,不反噬主写入。
|
|
1792
|
+
const nextAgentScope = p && p.agent_scope !== undefined ? normalizeExplicitScope(p.agent_scope) : undefined;
|
|
1793
|
+
const nextWorkspaceScope = p && p.workspace_scope !== undefined ? normalizeExplicitScope(p.workspace_scope) : undefined;
|
|
1794
|
+
const scopeChanged = nextAgentScope !== undefined || nextWorkspaceScope !== undefined;
|
|
1795
|
+
if (scopeChanged) {
|
|
1796
|
+
const decidedAt = new Date().toISOString();
|
|
1797
|
+
patch = { ...patch };
|
|
1798
|
+
if (nextAgentScope !== undefined) {
|
|
1799
|
+
patch.agent_scope = nextAgentScope;
|
|
1800
|
+
patch.agent_scope_source = "explicit";
|
|
1801
|
+
}
|
|
1802
|
+
if (nextWorkspaceScope !== undefined) {
|
|
1803
|
+
patch.workspace_scope = nextWorkspaceScope;
|
|
1804
|
+
patch.workspace_scope_source = "explicit";
|
|
1805
|
+
}
|
|
1806
|
+
patch.scope_decided_at = decidedAt;
|
|
1807
|
+
}
|
|
1808
|
+
const updated = store.update(id, patch);
|
|
1809
|
+
if (scopeChanged && old && updated) {
|
|
1810
|
+
try {
|
|
1811
|
+
store.saveScopeChange({
|
|
1812
|
+
memory_id: id,
|
|
1813
|
+
actor: ctx.actor === "panel" ? "panel" : "tool",
|
|
1814
|
+
prev_agent_scope: old.agent_scope ?? null,
|
|
1815
|
+
prev_workspace_scope: old.workspace_scope ?? null,
|
|
1816
|
+
next_agent_scope: updated.agent_scope ?? null,
|
|
1817
|
+
next_workspace_scope: updated.workspace_scope ?? null,
|
|
1818
|
+
agent_scope_source: updated.agent_scope_source ?? null,
|
|
1819
|
+
workspace_scope_source: updated.workspace_scope_source ?? null,
|
|
1820
|
+
decided_at: patch.scope_decided_at
|
|
1821
|
+
});
|
|
1822
|
+
} catch (e) {
|
|
1823
|
+
try { ctx.logger?.warn?.(`[dsh-mneme] scope change audit failed: ${String(e)}`); } catch { /* 同样不反噬 */ }
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
// Record a user correction when any meaningful field changed and the
|
|
1827
|
+
// reflection failure tracker is enabled. expected = what it became,
|
|
1828
|
+
// actual = what it was before; query (when provided) captures the
|
|
1829
|
+
// user's original intent so later reflection can reason about recall.
|
|
1830
|
+
const hasMeaningfulChange = old && updated && (
|
|
1831
|
+
old.content !== updated.content ||
|
|
1832
|
+
old.title !== updated.title ||
|
|
1833
|
+
old.importance !== updated.importance
|
|
1834
|
+
);
|
|
1835
|
+
if (hasMeaningfulChange && config.reflectionFailureTracking) {
|
|
1836
|
+
store.saveFailure({
|
|
1837
|
+
id: randomUUID(),
|
|
1838
|
+
query: ctx.query ?? null,
|
|
1839
|
+
expected: updated.content,
|
|
1840
|
+
actual: old.content,
|
|
1841
|
+
before: { title: old.title, content: old.content, importance: old.importance },
|
|
1842
|
+
failure_type: "user_correction",
|
|
1843
|
+
memory_id: id
|
|
1844
|
+
});
|
|
1845
|
+
}
|
|
1846
|
+
const sync = afterSync("write");
|
|
1847
|
+
notifyWrite();
|
|
1848
|
+
scheduleEmbed(updated);
|
|
1849
|
+
// Audit peer B: when the mirror sync failed, the store write landed but
|
|
1850
|
+
// the mirror did not converge — return an explicit degraded receipt rather
|
|
1851
|
+
// than a plain success. Non-enumerable so existing deepEqual assertions on
|
|
1852
|
+
// the memory shape keep passing.
|
|
1853
|
+
if (!sync?.success && !sync?.deferred) {
|
|
1854
|
+
Object.defineProperty(updated, "_mirror", {
|
|
1855
|
+
value: { status: "degraded", error: sync?.error ?? "mirror sync failed" },
|
|
1856
|
+
enumerable: false,
|
|
1857
|
+
configurable: true
|
|
1858
|
+
});
|
|
1859
|
+
}
|
|
1860
|
+
return updated;
|
|
1861
|
+
}
|
|
1862
|
+
|
|
1863
|
+
function archiveMemory(id, f) {
|
|
1864
|
+
const updated = store.setArchived(id, f);
|
|
1865
|
+
afterSync("write");
|
|
1866
|
+
return updated;
|
|
1867
|
+
}
|
|
1868
|
+
|
|
1511
1869
|
return {
|
|
1512
1870
|
saveWithDedupe,
|
|
1513
1871
|
recoverMirror,
|
|
@@ -1516,6 +1874,7 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1516
1874
|
injectCandidates,
|
|
1517
1875
|
mergeHumanEdits,
|
|
1518
1876
|
toApiList,
|
|
1877
|
+
isVisibleInScope,
|
|
1519
1878
|
transaction,
|
|
1520
1879
|
enqueue,
|
|
1521
1880
|
setDreamHook(fn) { dreamHook = fn; },
|
|
@@ -1568,53 +1927,9 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1568
1927
|
afterSync("write");
|
|
1569
1928
|
notifyWrite();
|
|
1570
1929
|
},
|
|
1571
|
-
update:
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
// 上(SIGNAL_TAGS,applyQualityDisposition 以并集写入),而这里整组替换
|
|
1575
|
-
// tags——任何带 tags 的更新都会抹掉这条唯一的审计线索(报告实测 150 条低分
|
|
1576
|
-
// 记忆里恰好缺的 2 条就是被 update 过的,并因此误判过归档来源)。把 existing
|
|
1577
|
-
// 的系统信号标签并集保留;用户自传的标签照常生效。
|
|
1578
|
-
const patch = (p && Array.isArray(p.tags) && old && Array.isArray(old.tags))
|
|
1579
|
-
? { ...p, tags: [...new Set([...p.tags, ...old.tags.filter((t) => SIGNAL_TAGS.includes(t))])] }
|
|
1580
|
-
: p;
|
|
1581
|
-
const updated = store.update(id, patch);
|
|
1582
|
-
// Record a user correction when any meaningful field changed and the
|
|
1583
|
-
// reflection failure tracker is enabled. expected = what it became,
|
|
1584
|
-
// actual = what it was before; query (when provided) captures the
|
|
1585
|
-
// user's original intent so later reflection can reason about recall.
|
|
1586
|
-
const hasMeaningfulChange = old && updated && (
|
|
1587
|
-
old.content !== updated.content ||
|
|
1588
|
-
old.title !== updated.title ||
|
|
1589
|
-
old.importance !== updated.importance
|
|
1590
|
-
);
|
|
1591
|
-
if (hasMeaningfulChange && config.reflectionFailureTracking) {
|
|
1592
|
-
store.saveFailure({
|
|
1593
|
-
id: randomUUID(),
|
|
1594
|
-
query: ctx.query ?? null,
|
|
1595
|
-
expected: updated.content,
|
|
1596
|
-
actual: old.content,
|
|
1597
|
-
before: { title: old.title, content: old.content, importance: old.importance },
|
|
1598
|
-
failure_type: "user_correction",
|
|
1599
|
-
memory_id: id
|
|
1600
|
-
});
|
|
1601
|
-
}
|
|
1602
|
-
const sync = afterSync("write");
|
|
1603
|
-
notifyWrite();
|
|
1604
|
-
scheduleEmbed(updated);
|
|
1605
|
-
// Audit peer B: when the mirror sync failed, the store write landed but
|
|
1606
|
-
// the mirror did not converge — return an explicit degraded receipt rather
|
|
1607
|
-
// than a plain success. Non-enumerable so existing deepEqual assertions on
|
|
1608
|
-
// the memory shape keep passing.
|
|
1609
|
-
if (!sync?.success && !sync?.deferred) {
|
|
1610
|
-
Object.defineProperty(updated, "_mirror", {
|
|
1611
|
-
value: { status: "degraded", error: sync?.error ?? "mirror sync failed" },
|
|
1612
|
-
enumerable: false,
|
|
1613
|
-
configurable: true
|
|
1614
|
-
});
|
|
1615
|
-
}
|
|
1616
|
-
return updated;
|
|
1617
|
-
},
|
|
1930
|
+
update: updateMemory,
|
|
1931
|
+
// v0.8.1 底座:scope 归属修正审计(单条记忆,新→旧)。
|
|
1932
|
+
listScopeChanges: (memoryId, opts) => store.listScopeChanges(memoryId, opts),
|
|
1618
1933
|
// Compare-and-set update: applies the patch only when the row still carries
|
|
1619
1934
|
// `expectedUpdatedAt`. Returns undefined on a miss (no write) so the caller
|
|
1620
1935
|
// can re-read and retry — the primitive that prevents lost updates across
|
|
@@ -1657,11 +1972,7 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1657
1972
|
afterSync("write");
|
|
1658
1973
|
return updated;
|
|
1659
1974
|
},
|
|
1660
|
-
setArchived:
|
|
1661
|
-
const updated = store.setArchived(id, f);
|
|
1662
|
-
afterSync("write");
|
|
1663
|
-
return updated;
|
|
1664
|
-
},
|
|
1975
|
+
setArchived: archiveMemory,
|
|
1665
1976
|
// sleep-mode storage (v0.4.0). demoteToSummary / restoreContent mutate
|
|
1666
1977
|
// content so they ride the normal write-hook path (mirror re-renders).
|
|
1667
1978
|
// touchLastAccess is a read-stamp — deliberately NO write hook (a recall
|
|
@@ -1693,7 +2004,8 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
1693
2004
|
// above: an audit write, never a write-hook-triggering memory mutation).
|
|
1694
2005
|
saveConflictPending: (r) => store.saveConflictPending(r),
|
|
1695
2006
|
listConflictPending: (opts) => store.listConflictPending(opts),
|
|
1696
|
-
|
|
2007
|
+
listConflictQueue,
|
|
2008
|
+
resolveConflictPending,
|
|
1697
2009
|
countConflictPending: () => store.countConflictPending(),
|
|
1698
2010
|
// Recall evaluation trail (方案 B): audit-bookkeeping semantics like the
|
|
1699
2011
|
// dream/recall passthroughs above — a recall_evals write is a snapshot, not
|
package/src/settings.js
CHANGED
|
@@ -64,6 +64,13 @@ const FEATURE_FLAG_BOOLEANS = [
|
|
|
64
64
|
// Issue #89:宽容校验回归(默认开)+ 跨类型合并显式放宽(默认关)。
|
|
65
65
|
"dreamSkipInvalid",
|
|
66
66
|
"allowCrossTypeMerge",
|
|
67
|
+
// Issue #17(v0.8.0 A1):scope 隔离存储层总开关。开启后写入标注
|
|
68
|
+
// agent_scope/workspace_scope、去重键扩展(含 sensitivity);检索侧过滤在
|
|
69
|
+
// A2/A3。默认关=行为与 A1 前逐字节一致。
|
|
70
|
+
"scopeEnabled",
|
|
71
|
+
// Issue #17(v0.8.0 A3):strictScope 硬过滤——他 scope 完全不可见(关闭时
|
|
72
|
+
// 为 A2 软隔离:降权保留可见)。
|
|
73
|
+
"strictScope",
|
|
67
74
|
// 嵌套对象开关:config.js 里是 memoryQualityFilter / llmAudit 对象的 enabled
|
|
68
75
|
// 子字段。kv 按点号键平铺存("memoryQualityFilter.enabled": false),index.js
|
|
69
76
|
// 合并时展开回嵌套对象,api.js 的 effective 从对象子字段取值。
|