@modusensus/dsh-mneme 0.8.3 → 0.8.5
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/LICENSE +21 -21
- package/README.md +727 -636
- package/bin/cli.mjs +0 -0
- package/bin/dsh-mneme-mcp.mjs +510 -0
- package/cordis.patch.yml +21 -21
- package/lib/api-standalone.js +506 -316
- package/lib/api.js +1403 -1316
- package/lib/bootstrap.js +211 -0
- package/lib/client.js +4657 -4366
- package/lib/commands.js +95 -95
- package/lib/config.js +615 -510
- package/lib/document.js +221 -0
- package/lib/dream/clustering.js +118 -118
- package/lib/dream/decisions.js +658 -658
- package/lib/dream/narratives.js +44 -0
- package/lib/dream/sleep.js +702 -700
- package/lib/dream.js +147 -16
- package/lib/embedding.js +177 -177
- package/lib/entities/extractor.js +249 -249
- package/lib/guide.js +44 -0
- package/lib/heat.js +144 -136
- package/lib/hot-memory.js +46 -46
- package/lib/index.js +515 -515
- package/lib/inject.js +375 -295
- package/lib/lang.js +208 -179
- package/lib/local-embedder.js +320 -320
- package/lib/mirror.js +198 -198
- package/lib/organize.js +450 -0
- package/lib/quality-filter.js +126 -126
- package/lib/recall-stats.js +125 -0
- package/lib/reranker.js +269 -269
- package/lib/runtime/adopt.js +176 -176
- package/lib/runtime/closure.js +172 -172
- package/lib/runtime/download.js +247 -247
- package/lib/runtime/layout.js +265 -235
- package/lib/runtime/loader.js +252 -249
- package/lib/runtime/model-fetch.js +361 -361
- package/lib/runtime/provision.js +237 -237
- package/lib/runtime/tarball.js +130 -130
- package/lib/runtime/verify.js +204 -191
- package/lib/scope.js +118 -118
- package/lib/search/adaptive.js +60 -22
- package/lib/search/bm25.js +96 -96
- package/lib/service.js +2324 -2087
- package/lib/settings.js +459 -420
- package/lib/store.js +2549 -2367
- package/lib/summarize.js +820 -449
- package/lib/tools.js +702 -574
- package/lib/vector-index.js +110 -110
- package/lib/version-check.js +122 -122
- package/package.json +109 -108
- package/runtime-manifest.json +526 -596
- package/scripts/benchmark-embed.js +201 -201
- package/scripts/benchmark-recall.js +199 -199
- package/scripts/benchmark-rerank.js +166 -166
- package/scripts/build-runtime-manifest.mjs +224 -217
- package/scripts/check-sync.js +42 -42
- package/scripts/e2e-dsh.js +220 -220
- package/scripts/mneme-runtime.mjs +277 -277
- package/scripts/stress-dsh.js +255 -255
- package/scripts/sync-lib.js +47 -47
- package/scripts/sync-test-badge.mjs +67 -61
- package/src/api-standalone.js +506 -316
- package/src/api.js +1403 -1316
- package/src/bootstrap.js +211 -0
- package/src/commands.js +95 -95
- package/src/config.js +615 -510
- package/src/document.js +221 -0
- package/src/dream/clustering.js +118 -118
- package/src/dream/decisions.js +658 -658
- package/src/dream/narratives.js +44 -0
- package/src/dream/sleep.js +702 -700
- package/src/dream.js +147 -16
- package/src/embedding.js +177 -177
- package/src/entities/extractor.js +249 -249
- package/src/guide.js +44 -0
- package/src/heat.js +144 -136
- package/src/hot-memory.js +46 -46
- package/src/index.js +515 -515
- package/src/inject.js +375 -295
- package/src/lang.js +208 -179
- package/src/local-embedder.js +320 -320
- package/src/mirror.js +198 -198
- package/src/organize.js +450 -0
- package/src/quality-filter.js +126 -126
- package/src/recall-stats.js +125 -0
- package/src/reranker.js +269 -269
- package/src/runtime/adopt.js +176 -176
- package/src/runtime/closure.js +172 -172
- package/src/runtime/download.js +247 -247
- package/src/runtime/layout.js +265 -235
- package/src/runtime/loader.js +252 -249
- package/src/runtime/model-fetch.js +361 -361
- package/src/runtime/provision.js +237 -237
- package/src/runtime/tarball.js +130 -130
- package/src/runtime/verify.js +204 -191
- package/src/scope.js +118 -118
- package/src/search/adaptive.js +60 -22
- package/src/search/bm25.js +96 -96
- package/src/service.js +2324 -2087
- package/src/settings.js +459 -420
- package/src/store.js +2549 -2367
- package/src/summarize.js +820 -449
- package/src/tools.js +702 -574
- package/src/vector-index.js +110 -110
- package/src/version-check.js +122 -122
- package/test/adaptive-inject-budget.test.js +85 -0
- package/test/api-routes-gaps.test.js +218 -218
- package/test/api.test.js +1640 -1482
- package/test/audit.test.js +448 -448
- package/test/benchmark.test.js +94 -94
- package/test/bootstrap.test.js +220 -0
- package/test/client.test.js +860 -736
- package/test/clustering.test.js +100 -100
- package/test/commands.test.js +106 -106
- package/test/config.test.js +66 -66
- package/test/conflict-freeze.test.js +300 -300
- package/test/document.test.js +523 -0
- package/test/dream-archive-guardrail.test.js +116 -116
- package/test/dream-candidate-hybrid.test.js +281 -281
- package/test/dream-coverage-degrade.test.js +86 -86
- package/test/dream.test.js +1308 -1254
- package/test/embedding-ready.test.js +196 -196
- package/test/entities.test.js +547 -547
- package/test/entity-recall.test.js +179 -0
- package/test/epistemic.test.js +298 -298
- package/test/fnew-0112.test.js +311 -311
- package/test/fnew-03.test.js +422 -422
- package/test/graph-api.test.js +175 -175
- package/test/heat.test.js +175 -148
- package/test/helpers/dream-mock.js +82 -82
- package/test/helpers/peer-worker.mjs +25 -25
- package/test/helpers/runtime-source.js +60 -60
- package/test/helpers/tar-builder.js +80 -80
- package/test/hot-memory.test.js +145 -145
- package/test/inject-content-cap.test.js +91 -0
- package/test/inject-first-round-bm25.test.js +89 -89
- package/test/inject-heat.test.js +61 -0
- package/test/inject-pools.test.js +170 -0
- package/test/inject-recall.test.js +105 -0
- package/test/inject-rotation.test.js +161 -161
- package/test/inject.test.js +283 -249
- package/test/lang.test.js +87 -87
- package/test/lib-smoke.test.js +96 -96
- package/test/llm-audit.test.js +462 -309
- package/test/llm-message-source.test.js +177 -177
- package/test/local-embedder.test.js +249 -249
- package/test/mcp-stdio.test.js +385 -0
- package/test/mirror-dirty.test.js +424 -424
- package/test/mirror-edit-digest.test.js +187 -187
- package/test/mirror-generation.test.js +499 -499
- package/test/mirror.test.js +249 -249
- package/test/narrative-bars.test.js +162 -0
- package/test/organize.test.js +418 -0
- package/test/peer-blockers.test.js +193 -193
- package/test/policy-epoch.test.js +259 -259
- package/test/quality-filter.test.js +186 -186
- package/test/reasoning-effort.test.js +746 -746
- package/test/recall-evals.test.js +235 -235
- package/test/recall-layer.test.js +326 -326
- package/test/recall-stats.test.js +123 -0
- package/test/receipt-chain.test.js +451 -451
- package/test/reflection.test.js +226 -226
- package/test/reindex-backfill.test.js +62 -62
- package/test/reranker-single-logit.test.js +73 -73
- package/test/reranker.test.js +197 -197
- package/test/resident-status.test.js +83 -0
- package/test/runtime-adopt.test.js +206 -206
- package/test/runtime-cli.test.js +89 -89
- package/test/runtime-closure.test.js +193 -193
- package/test/runtime-download.test.js +302 -302
- package/test/runtime-layout.test.js +163 -163
- package/test/runtime-loader.test.js +255 -237
- package/test/runtime-manifest.test.js +159 -158
- package/test/runtime-model-fetch.test.js +348 -348
- package/test/runtime-provision.test.js +101 -101
- package/test/runtime-tarball.test.js +81 -81
- package/test/runtime-verify.test.js +279 -214
- package/test/scope-candidates.test.js +239 -239
- package/test/scope-explicit.test.js +452 -452
- package/test/scope-retrieval.test.js +197 -197
- package/test/scope-storage.test.js +255 -255
- package/test/scope-strict.test.js +231 -231
- package/test/search-fusion.test.js +90 -90
- package/test/semantic.test.js +124 -124
- package/test/service-search.test.js +174 -174
- package/test/service.test.js +405 -405
- package/test/settings.test.js +261 -261
- package/test/skip-invalid-restore.test.js +262 -262
- package/test/sleep-action-set.test.js +422 -422
- package/test/sleep-heat.test.js +126 -125
- package/test/sleep-scheduler-rearm.test.js +89 -89
- package/test/sleep.test.js +410 -373
- package/test/standalone-api.test.js +544 -359
- package/test/store-vector-cache.test.js +98 -98
- package/test/store.test.js +396 -383
- package/test/stress.test.js +209 -209
- package/test/summarize.test.js +1288 -575
- package/test/tools-dto-schema-matrix.test.js +325 -0
- package/test/tools-memory-get-schema.test.js +65 -65
- package/test/tools.test.js +374 -341
- package/test/updated-at-semantics.test.js +113 -113
- package/test/vector-index.test.js +221 -221
- package/test/version-check.test.js +152 -152
- package/test/webServer-optional.test.js +82 -82
package/src/document.js
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
// dsh-mneme/src/document.js
|
|
2
|
+
// document 型记忆注册(issue #230,口径基线 = #164 设计稿评审线):agent 产
|
|
3
|
+
// 长文档的指针行铸造口。写入权分离——全文归 agent(管线对文件零读零写零改),
|
|
4
|
+
// 管线只做四件事:注册校验(文件存在 + 路径合法 + evidence 求交)、摘要 +
|
|
5
|
+
// doc_path 落库(库里只存这三样)、C2 vector 档比对去重、supersede 记账
|
|
6
|
+
// (出新版 = 新摘要行 supersede 旧行,旧文件不删,content_history 可追溯)。
|
|
7
|
+
//
|
|
8
|
+
// 独立成模块(AGENTS.md 尺寸约定,同 recallStats 先例):service.js 已过
|
|
9
|
+
// 2000 行参考线,这里只依赖注入 service 内部件,barrel 出口在 service.js,
|
|
10
|
+
// 调用方零改动。防绕过三层兜底:service 两条业务守卫(saveWithDedupe /
|
|
11
|
+
// updateMemory)+ 存储层唯一铸造口(store.saveDocument,通用 save/update/
|
|
12
|
+
// CAS 整类拒绝 document)——注册是唯一铸造口。
|
|
13
|
+
|
|
14
|
+
import { statSync } from "node:fs";
|
|
15
|
+
import { homedir } from "node:os";
|
|
16
|
+
import { isAbsolute, resolve } from "node:path";
|
|
17
|
+
import { scopeKeyOf } from "./scope.js";
|
|
18
|
+
import { cosineSimilarity } from "./dream/clustering.js";
|
|
19
|
+
|
|
20
|
+
// C2 vector 档阈值(#164 拍板 0.92,与 findSessionDuplicate 的 vector 档同源)。
|
|
21
|
+
// 刻意不做 24h 时间窗:那个窗是会话去重的「近邻界」(findSessionDuplicate 注释),
|
|
22
|
+
// 不是相似度语义本身;document 版本更新天然跨天/跨周,套窗会把一周后重注册的
|
|
23
|
+
// 新版判成「无近重复」,留下两行活跃摘要漂移——supersede 探测必须全时段。
|
|
24
|
+
const DOCUMENT_MIN_SIM = 0.92;
|
|
25
|
+
const CANDIDATE_LIMIT = 200;
|
|
26
|
+
|
|
27
|
+
// 追加到被取代行正文的指针注记。刻意不带语言分支:id 是跨语言稳定键(用户
|
|
28
|
+
// 对 tag/章节名建议用英文同理——指针性内容不翻译);可追溯链 = 注记里的新行
|
|
29
|
+
// id + 旧行 content_history 里的旧摘要(source=superseded)。
|
|
30
|
+
const supersededByPointer = (id) => `\n\n[superseded by ${id}]`;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Build the document registrar. Injected deps are service.js closure members
|
|
34
|
+
* (store / embedQuery / transaction / finalize) so this module stays free of
|
|
35
|
+
* service-internal wiring; `finalize` is the single write epilogue (mirror
|
|
36
|
+
* sync + notify + re-embed) so callers never duplicate it.
|
|
37
|
+
*
|
|
38
|
+
* @returns {(payload: object) => Promise<object>}
|
|
39
|
+
* `{ action: "created"|"superseded", memory, superseded?, evidence_kept,
|
|
40
|
+
* evidence_dropped, degraded }`
|
|
41
|
+
* @throws flag 关 / 路径非法或文件缺失 / title 或 summary 为空 / evidence 全部
|
|
42
|
+
* 捏造 / 仅 vector 近重复(疑似重复外档,交 agent 裁决而不是静默二选一)。
|
|
43
|
+
*/
|
|
44
|
+
export function createDocumentRegistrar({ store, config, embedQuery, pushContentHistory, transaction, finalize }) {
|
|
45
|
+
return async function registerDocument(payload, { hiddenEvidenceIds = [] } = {}) {
|
|
46
|
+
// opt-in 总闸(#230 对齐 #228 形态):关 = 注册入口整体不存在,错误信息
|
|
47
|
+
// 指回配置键,agent 能准确转告用户去开。
|
|
48
|
+
if (config?.documentMemoryEnabled !== true) {
|
|
49
|
+
throw new Error("document memory is disabled (documentMemoryEnabled)");
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// --- 1. 路径合法 + 文件存在 --------------------------------------------
|
|
53
|
+
// 只认展开 ~ 后的绝对路径:相对路径依赖进程 cwd,注册时的 cwd 与读取时
|
|
54
|
+
// 的 cwd 不保证一致,指针必须自带完整定位。校验存在 + 常规文件 + 非空——
|
|
55
|
+
// 空文件/被截断成 0 字节是「文字结构意外破坏」的最早信号,注册时拦住
|
|
56
|
+
// 而不是落一条死指针(回退路径:agent 修好文件后重注册,同 path/title
|
|
57
|
+
// 自动 supersede 旧行)。
|
|
58
|
+
const rawPath = String(payload?.path ?? "").trim();
|
|
59
|
+
if (!rawPath) throw new Error("registerDocument: path is required");
|
|
60
|
+
// 只展开 ~ / ~/(~user 形式拒收为普通相对路径走 isAbsolute 拦截——不猜测
|
|
61
|
+
// 其他用户的 home,错展开比报错更糟)。
|
|
62
|
+
const isTilde = rawPath === "~" || rawPath.startsWith("~/") || rawPath.startsWith("~\\");
|
|
63
|
+
const absolute = isTilde ? resolve(homedir(), rawPath.slice(2)) : rawPath;
|
|
64
|
+
if (!isAbsolute(absolute)) {
|
|
65
|
+
throw new Error(`registerDocument: path must be absolute (got "${rawPath}")`);
|
|
66
|
+
}
|
|
67
|
+
// 所有绝对路径统一 resolve 归一化(. / .. / 重复分隔符)——归一化不做的话,
|
|
68
|
+
// D:\a\.\b.md 与 D:\a\b.md 会被判成两个路径,同文件绕过 same-path
|
|
69
|
+
// supersede,留下双活跃指针。realpathSync 刻意不用:符号链接合并会改写
|
|
70
|
+
// agent 提交的路径字面量,指针的可读性比追符号链更重要。
|
|
71
|
+
const expanded = resolve(absolute);
|
|
72
|
+
let stat = null;
|
|
73
|
+
try { stat = statSync(expanded); } catch { /* missing/unreadable → null */ }
|
|
74
|
+
if (!stat?.isFile() || stat.size <= 0) {
|
|
75
|
+
throw new Error(`registerDocument: document file not found or empty: ${expanded}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// --- 2. evidence 求交(sleep.js 扫描集求交同款语义)---------------------
|
|
79
|
+
// 引用的原子条必须真实存在且未归档(归档行已被降级,不再为新鲜 document
|
|
80
|
+
// 背书)。红线 4 宽容形态:合法子集落库 + evidence_degraded 系统标记;
|
|
81
|
+
// 提供了 evidence 但全军覆没 = 捏造信号,整单拒绝,agent 修正后重试。
|
|
82
|
+
const wanted = [...new Set(
|
|
83
|
+
(Array.isArray(payload?.evidence) ? payload.evidence : [])
|
|
84
|
+
.map((id) => String(id ?? "").trim())
|
|
85
|
+
.filter(Boolean)
|
|
86
|
+
)];
|
|
87
|
+
// strictScope(tools 层传入,#170 复核项 4 同款):调用者 scope 看不见的
|
|
88
|
+
// 行按「不存在」处理——不得为跨 scope id 建立引用(存在性泄漏),与
|
|
89
|
+
// unknown/archived 同落 dropped,evidence_kept/dropped 计数保持诚实。
|
|
90
|
+
const hidden = new Set((Array.isArray(hiddenEvidenceIds) ? hiddenEvidenceIds : []).map((id) => String(id)));
|
|
91
|
+
const kept = [];
|
|
92
|
+
const dropped = [];
|
|
93
|
+
for (const id of wanted) {
|
|
94
|
+
const row = hidden.has(id) ? null : store.getById(id);
|
|
95
|
+
(row && !row.archived ? kept : dropped).push(id);
|
|
96
|
+
}
|
|
97
|
+
if (wanted.length > 0 && kept.length === 0) {
|
|
98
|
+
throw new Error(
|
|
99
|
+
`registerDocument: all ${wanted.length} evidence ids are unknown, archived or out of scope — fabricated evidence is rejected`
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const title = String(payload?.title ?? "").trim();
|
|
104
|
+
if (!title) throw new Error("registerDocument: title is required");
|
|
105
|
+
const summary = String(payload?.summary ?? "").trim();
|
|
106
|
+
if (!summary) throw new Error("registerDocument: summary is required");
|
|
107
|
+
|
|
108
|
+
// --- 3. supersede 目标探测(三层)---------------------------------------
|
|
109
|
+
// 同 doc_path / 同标题 = agent 显式的「出新版」意图 → supersede;仅 vector
|
|
110
|
+
// 近重复而路径标题都不同 = 疑似重复外档 → 拒绝并指路。C2×C1 硬线:比对
|
|
111
|
+
// 不得越过矛盾裁决替 agent 二选一——改既有行还是归档后重注册,判断权留在
|
|
112
|
+
// agent 手里。去重键带 scope(scopeKeyOf):跨作用域永不互判(防泄漏,与
|
|
113
|
+
// saveWithDedupe 的 scopeMatches 同口径)。
|
|
114
|
+
const scopeMatches = (m) =>
|
|
115
|
+
scopeKeyOf(m.agent_scope) === scopeKeyOf(payload?.agent_scope) &&
|
|
116
|
+
scopeKeyOf(m.workspace_scope) === scopeKeyOf(payload?.workspace_scope) &&
|
|
117
|
+
scopeKeyOf(m.sensitivity) === scopeKeyOf(payload?.sensitivity);
|
|
118
|
+
const samePath = (a, b) => {
|
|
119
|
+
if (!a || !b) return false;
|
|
120
|
+
// Windows 文件系统大小写不敏感:比较前归一,否则 C:\a.md 与 c:\A.md
|
|
121
|
+
// 会被判成两个版本。
|
|
122
|
+
const norm = (p) => (process.platform === "win32" ? String(p).toLowerCase() : String(p));
|
|
123
|
+
return norm(a) === norm(b);
|
|
124
|
+
};
|
|
125
|
+
// 精确层(同路径/同标题)必须全量扫描、不允许窗口截断:>200 行活跃
|
|
126
|
+
// document 时,窗口外的同路径旧版漏检会留下双活跃版本——supersede 判定
|
|
127
|
+
// 是正确性要求,不是性能优化对象。vector 档比对保留 CANDIDATE_LIMIT
|
|
128
|
+
// (getEmbeddings + 两两 cosine 才是有界对象)。
|
|
129
|
+
const scopeMatched = store
|
|
130
|
+
.list({ type: "document", limit: null })
|
|
131
|
+
.filter((m) => scopeMatches(m));
|
|
132
|
+
const explicitTarget = scopeMatched.find((m) => samePath(m.doc_path, expanded))
|
|
133
|
+
?? scopeMatched.find((m) => m.title.trim() === title);
|
|
134
|
+
if (!explicitTarget) {
|
|
135
|
+
// vector 档:embedder 不可用/向量缺失一律跳过——去重是增强不是写入依赖
|
|
136
|
+
// (findSessionDuplicate 同原则)。probe 用 title+summary,与行向量
|
|
137
|
+
// (同两段拼接 embed)同构。
|
|
138
|
+
const candidates = scopeMatched.slice(0, CANDIDATE_LIMIT);
|
|
139
|
+
try {
|
|
140
|
+
const vecs = store.getEmbeddings(candidates.map((m) => m.id));
|
|
141
|
+
if (vecs.size) {
|
|
142
|
+
const probe = await embedQuery([title, summary].filter(Boolean).join("\n"));
|
|
143
|
+
if (probe) {
|
|
144
|
+
let best = null;
|
|
145
|
+
for (const m of candidates) {
|
|
146
|
+
const v = vecs.get(m.id);
|
|
147
|
+
if (!v) continue; // 无向量的既有行不参与比对(无信号 = 不判定)
|
|
148
|
+
const sim = cosineSimilarity(probe, v);
|
|
149
|
+
if (sim >= DOCUMENT_MIN_SIM && (!best || sim > best.sim)) best = { m, sim };
|
|
150
|
+
}
|
|
151
|
+
if (best) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`registerDocument: summary is a near duplicate (C2 >= ${DOCUMENT_MIN_SIM}, sim ${best.sim.toFixed(3)}) ` +
|
|
154
|
+
`of document row ${best.m.id} "${best.m.title}" — update that row via memory_update, or archive it ` +
|
|
155
|
+
`before registering a separate document`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
} catch (e) {
|
|
161
|
+
// 近重复拒绝是业务结果,原样上抛;embedder 故障吞掉回落(只做路径/标题层)。
|
|
162
|
+
if (e instanceof Error && e.message.startsWith("registerDocument:")) throw e;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// --- 4. 落库 + supersede 记账(同事务)----------------------------------
|
|
167
|
+
// 新摘要行:库里只存 summary + doc_path + evidence(+ scope 标注),旧文件
|
|
168
|
+
// 零触碰。supersede:旧行 content_history 存旧摘要(source=superseded)、
|
|
169
|
+
// 正文追加指向新行的注记、归档(降可及性可复活,无物理删除)。
|
|
170
|
+
const userTags = (Array.isArray(payload?.tags) ? payload.tags : [])
|
|
171
|
+
.map((t) => String(t ?? "").trim())
|
|
172
|
+
.filter(Boolean);
|
|
173
|
+
const tags = [...new Set([...userTags, ...(dropped.length ? ["evidence_degraded"] : [])])];
|
|
174
|
+
const result = transaction(() => {
|
|
175
|
+
// saveDocument 是存储层唯一 document 铸造口(通用 save/update/CAS 拒绝
|
|
176
|
+
// document——CodeRabbit 复核 #8/#882):registerDocument 传入的 doc_path
|
|
177
|
+
// 已过归一化与文件校验。
|
|
178
|
+
const created = store.saveDocument({
|
|
179
|
+
type: "document",
|
|
180
|
+
title,
|
|
181
|
+
content: summary,
|
|
182
|
+
tags,
|
|
183
|
+
importance: Number.isInteger(payload?.importance)
|
|
184
|
+
? Math.min(5, Math.max(1, payload.importance))
|
|
185
|
+
: 3,
|
|
186
|
+
source: payload?.source ?? "tool",
|
|
187
|
+
evidence: kept,
|
|
188
|
+
doc_path: expanded,
|
|
189
|
+
// sensitivity 进 supersede 匹配键(scopeKeyOf 第三维):不落库的话,
|
|
190
|
+
// 首注册带 sensitivity、重注册同键会在匹配时判成两 scope,旧版漏检。
|
|
191
|
+
...(payload?.sensitivity !== undefined ? { sensitivity: payload.sensitivity } : {}),
|
|
192
|
+
...(payload?.agent_scope !== undefined
|
|
193
|
+
? { agent_scope: payload.agent_scope, agent_scope_source: payload.agent_scope_source }
|
|
194
|
+
: {}),
|
|
195
|
+
...(payload?.workspace_scope !== undefined
|
|
196
|
+
? { workspace_scope: payload.workspace_scope, workspace_scope_source: payload.workspace_scope_source }
|
|
197
|
+
: {})
|
|
198
|
+
});
|
|
199
|
+
let superseded;
|
|
200
|
+
const loser = explicitTarget ? store.getById(explicitTarget.id) : null;
|
|
201
|
+
if (loser && !loser.archived) {
|
|
202
|
+
store.update(loser.id, {
|
|
203
|
+
content: `${loser.content}${supersededByPointer(created.id)}`,
|
|
204
|
+
content_history: pushContentHistory(loser, "superseded")
|
|
205
|
+
});
|
|
206
|
+
store.setArchived(loser.id, true);
|
|
207
|
+
superseded = store.getById(loser.id);
|
|
208
|
+
}
|
|
209
|
+
return { created, superseded };
|
|
210
|
+
});
|
|
211
|
+
finalize(result.superseded ? [result.created, result.superseded] : [result.created]);
|
|
212
|
+
return {
|
|
213
|
+
action: result.superseded ? "superseded" : "created",
|
|
214
|
+
memory: result.created,
|
|
215
|
+
...(result.superseded ? { superseded: result.superseded } : {}),
|
|
216
|
+
evidence_kept: kept.length,
|
|
217
|
+
evidence_dropped: dropped.length,
|
|
218
|
+
degraded: dropped.length > 0
|
|
219
|
+
};
|
|
220
|
+
};
|
|
221
|
+
}
|
package/src/dream/clustering.js
CHANGED
|
@@ -1,118 +1,118 @@
|
|
|
1
|
-
// Cosine similarity of two equal-length numeric vectors. Returns 0 for empty
|
|
2
|
-
// or mismatched-length inputs, and for zero-norm vectors (numerically stable).
|
|
3
|
-
export function cosineSimilarity(a, b) {
|
|
4
|
-
if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || a.length !== b.length) return 0;
|
|
5
|
-
let dot = 0, na = 0, nb = 0;
|
|
6
|
-
for (let i = 0; i < a.length; i++) {
|
|
7
|
-
dot += a[i] * b[i];
|
|
8
|
-
na += a[i] * a[i];
|
|
9
|
-
nb += b[i] * b[i];
|
|
10
|
-
}
|
|
11
|
-
if (na === 0 || nb === 0) return 0;
|
|
12
|
-
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
// K-Means clustering of numeric vectors. Returns one index array per cluster.
|
|
16
|
-
// Uses k-means++ seeding and repairs empty clusters mid-iteration.
|
|
17
|
-
export function kMeans(vectors, k, opts = {}) {
|
|
18
|
-
const n = vectors.length;
|
|
19
|
-
if (n === 0) return [];
|
|
20
|
-
const { maxIter = 100, tol = 1e-4 } = opts ?? {};
|
|
21
|
-
if (k <= 0 || k >= n) return [vectors.map((_, i) => i)];
|
|
22
|
-
|
|
23
|
-
const dim = vectors[0].length;
|
|
24
|
-
const randomIndex = () => Math.floor(Math.random() * n);
|
|
25
|
-
const squaredDist = (v, c) => {
|
|
26
|
-
let s = 0;
|
|
27
|
-
for (let d = 0; d < dim; d++) s += (v[d] - c[d]) * (v[d] - c[d]);
|
|
28
|
-
return s;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
// k-means++ seeding: first centroid uniform-random, later centroids sampled
|
|
32
|
-
// with probability proportional to squared distance from nearest centroid.
|
|
33
|
-
const centroids = [[...vectors[randomIndex()]]];
|
|
34
|
-
while (centroids.length < k) {
|
|
35
|
-
const dist = new Array(n);
|
|
36
|
-
let sum = 0;
|
|
37
|
-
for (let i = 0; i < n; i++) {
|
|
38
|
-
let best = Infinity;
|
|
39
|
-
for (const c of centroids) best = Math.min(best, squaredDist(vectors[i], c));
|
|
40
|
-
dist[i] = best;
|
|
41
|
-
sum += best;
|
|
42
|
-
}
|
|
43
|
-
if (sum === 0) {
|
|
44
|
-
// All points coincide with a centroid; fall back to uniform random.
|
|
45
|
-
centroids.push([...vectors[randomIndex()]]);
|
|
46
|
-
} else {
|
|
47
|
-
let r = Math.random() * sum;
|
|
48
|
-
let pick = 0;
|
|
49
|
-
for (let i = 0; i < n; i++) {
|
|
50
|
-
r -= dist[i];
|
|
51
|
-
if (r <= 0) { pick = i; break; }
|
|
52
|
-
}
|
|
53
|
-
centroids.push([...vectors[pick]]);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const assignment = new Array(n).fill(0);
|
|
58
|
-
for (let iter = 0; iter < maxIter; iter++) {
|
|
59
|
-
// Assign each point to its nearest centroid.
|
|
60
|
-
let changed = false;
|
|
61
|
-
for (let i = 0; i < n; i++) {
|
|
62
|
-
let best = 0, bestD = Infinity;
|
|
63
|
-
for (let j = 0; j < k; j++) {
|
|
64
|
-
const d = squaredDist(vectors[i], centroids[j]);
|
|
65
|
-
if (d < bestD) { bestD = d; best = j; }
|
|
66
|
-
}
|
|
67
|
-
if (assignment[i] !== best) { assignment[i] = best; changed = true; }
|
|
68
|
-
}
|
|
69
|
-
if (!changed) break; // stable assignment => converged.
|
|
70
|
-
|
|
71
|
-
// Recompute centroids as cluster means; reseed empty clusters randomly.
|
|
72
|
-
const sums = Array.from({ length: k }, () => new Array(dim).fill(0));
|
|
73
|
-
const counts = new Array(k).fill(0);
|
|
74
|
-
for (let i = 0; i < n; i++) {
|
|
75
|
-
counts[assignment[i]]++;
|
|
76
|
-
for (let d = 0; d < dim; d++) sums[assignment[i]][d] += vectors[i][d];
|
|
77
|
-
}
|
|
78
|
-
let maxMove = 0;
|
|
79
|
-
for (let j = 0; j < k; j++) {
|
|
80
|
-
if (counts[j] === 0) {
|
|
81
|
-
centroids[j] = [...vectors[randomIndex()]];
|
|
82
|
-
maxMove = Infinity; // never call this converged this round.
|
|
83
|
-
} else {
|
|
84
|
-
for (let d = 0; d < dim; d++) {
|
|
85
|
-
const mean = sums[j][d] / counts[j];
|
|
86
|
-
const move = mean - centroids[j][d];
|
|
87
|
-
if (move * move > maxMove) maxMove = move * move;
|
|
88
|
-
centroids[j][d] = mean;
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
if (Math.sqrt(maxMove) < tol) break; // centroids moved less than tol.
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
// Build clusters from final assignments.
|
|
96
|
-
const clusters = Array.from({ length: k }, () => []);
|
|
97
|
-
for (let i = 0; i < n; i++) clusters[assignment[i]].push(i);
|
|
98
|
-
return clusters;
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// Group memories by vector clustering, preserving original object references.
|
|
102
|
-
export function clusterMemories(memories, vectors, k) {
|
|
103
|
-
return kMeans(vectors, k).map((indices) => indices.map((i) => memories[i]));
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// Find memory pairs that are highly similar yet could contradict each other.
|
|
107
|
-
// Only same-type pairs count; each pair reported once, lower index first.
|
|
108
|
-
export function findPotentialConflicts(memories, vectors, threshold = 0.85) {
|
|
109
|
-
const pairs = [];
|
|
110
|
-
for (let i = 0; i < memories.length; i++) {
|
|
111
|
-
for (let j = i + 1; j < memories.length; j++) {
|
|
112
|
-
if (memories[i].type !== memories[j].type) continue;
|
|
113
|
-
const sim = cosineSimilarity(vectors[i], vectors[j]);
|
|
114
|
-
if (sim > threshold) pairs.push({ a: memories[i], b: memories[j], similarity: sim });
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
return pairs;
|
|
118
|
-
}
|
|
1
|
+
// Cosine similarity of two equal-length numeric vectors. Returns 0 for empty
|
|
2
|
+
// or mismatched-length inputs, and for zero-norm vectors (numerically stable).
|
|
3
|
+
export function cosineSimilarity(a, b) {
|
|
4
|
+
if (!Array.isArray(a) || !Array.isArray(b) || a.length === 0 || a.length !== b.length) return 0;
|
|
5
|
+
let dot = 0, na = 0, nb = 0;
|
|
6
|
+
for (let i = 0; i < a.length; i++) {
|
|
7
|
+
dot += a[i] * b[i];
|
|
8
|
+
na += a[i] * a[i];
|
|
9
|
+
nb += b[i] * b[i];
|
|
10
|
+
}
|
|
11
|
+
if (na === 0 || nb === 0) return 0;
|
|
12
|
+
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// K-Means clustering of numeric vectors. Returns one index array per cluster.
|
|
16
|
+
// Uses k-means++ seeding and repairs empty clusters mid-iteration.
|
|
17
|
+
export function kMeans(vectors, k, opts = {}) {
|
|
18
|
+
const n = vectors.length;
|
|
19
|
+
if (n === 0) return [];
|
|
20
|
+
const { maxIter = 100, tol = 1e-4 } = opts ?? {};
|
|
21
|
+
if (k <= 0 || k >= n) return [vectors.map((_, i) => i)];
|
|
22
|
+
|
|
23
|
+
const dim = vectors[0].length;
|
|
24
|
+
const randomIndex = () => Math.floor(Math.random() * n);
|
|
25
|
+
const squaredDist = (v, c) => {
|
|
26
|
+
let s = 0;
|
|
27
|
+
for (let d = 0; d < dim; d++) s += (v[d] - c[d]) * (v[d] - c[d]);
|
|
28
|
+
return s;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// k-means++ seeding: first centroid uniform-random, later centroids sampled
|
|
32
|
+
// with probability proportional to squared distance from nearest centroid.
|
|
33
|
+
const centroids = [[...vectors[randomIndex()]]];
|
|
34
|
+
while (centroids.length < k) {
|
|
35
|
+
const dist = new Array(n);
|
|
36
|
+
let sum = 0;
|
|
37
|
+
for (let i = 0; i < n; i++) {
|
|
38
|
+
let best = Infinity;
|
|
39
|
+
for (const c of centroids) best = Math.min(best, squaredDist(vectors[i], c));
|
|
40
|
+
dist[i] = best;
|
|
41
|
+
sum += best;
|
|
42
|
+
}
|
|
43
|
+
if (sum === 0) {
|
|
44
|
+
// All points coincide with a centroid; fall back to uniform random.
|
|
45
|
+
centroids.push([...vectors[randomIndex()]]);
|
|
46
|
+
} else {
|
|
47
|
+
let r = Math.random() * sum;
|
|
48
|
+
let pick = 0;
|
|
49
|
+
for (let i = 0; i < n; i++) {
|
|
50
|
+
r -= dist[i];
|
|
51
|
+
if (r <= 0) { pick = i; break; }
|
|
52
|
+
}
|
|
53
|
+
centroids.push([...vectors[pick]]);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const assignment = new Array(n).fill(0);
|
|
58
|
+
for (let iter = 0; iter < maxIter; iter++) {
|
|
59
|
+
// Assign each point to its nearest centroid.
|
|
60
|
+
let changed = false;
|
|
61
|
+
for (let i = 0; i < n; i++) {
|
|
62
|
+
let best = 0, bestD = Infinity;
|
|
63
|
+
for (let j = 0; j < k; j++) {
|
|
64
|
+
const d = squaredDist(vectors[i], centroids[j]);
|
|
65
|
+
if (d < bestD) { bestD = d; best = j; }
|
|
66
|
+
}
|
|
67
|
+
if (assignment[i] !== best) { assignment[i] = best; changed = true; }
|
|
68
|
+
}
|
|
69
|
+
if (!changed) break; // stable assignment => converged.
|
|
70
|
+
|
|
71
|
+
// Recompute centroids as cluster means; reseed empty clusters randomly.
|
|
72
|
+
const sums = Array.from({ length: k }, () => new Array(dim).fill(0));
|
|
73
|
+
const counts = new Array(k).fill(0);
|
|
74
|
+
for (let i = 0; i < n; i++) {
|
|
75
|
+
counts[assignment[i]]++;
|
|
76
|
+
for (let d = 0; d < dim; d++) sums[assignment[i]][d] += vectors[i][d];
|
|
77
|
+
}
|
|
78
|
+
let maxMove = 0;
|
|
79
|
+
for (let j = 0; j < k; j++) {
|
|
80
|
+
if (counts[j] === 0) {
|
|
81
|
+
centroids[j] = [...vectors[randomIndex()]];
|
|
82
|
+
maxMove = Infinity; // never call this converged this round.
|
|
83
|
+
} else {
|
|
84
|
+
for (let d = 0; d < dim; d++) {
|
|
85
|
+
const mean = sums[j][d] / counts[j];
|
|
86
|
+
const move = mean - centroids[j][d];
|
|
87
|
+
if (move * move > maxMove) maxMove = move * move;
|
|
88
|
+
centroids[j][d] = mean;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (Math.sqrt(maxMove) < tol) break; // centroids moved less than tol.
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Build clusters from final assignments.
|
|
96
|
+
const clusters = Array.from({ length: k }, () => []);
|
|
97
|
+
for (let i = 0; i < n; i++) clusters[assignment[i]].push(i);
|
|
98
|
+
return clusters;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Group memories by vector clustering, preserving original object references.
|
|
102
|
+
export function clusterMemories(memories, vectors, k) {
|
|
103
|
+
return kMeans(vectors, k).map((indices) => indices.map((i) => memories[i]));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Find memory pairs that are highly similar yet could contradict each other.
|
|
107
|
+
// Only same-type pairs count; each pair reported once, lower index first.
|
|
108
|
+
export function findPotentialConflicts(memories, vectors, threshold = 0.85) {
|
|
109
|
+
const pairs = [];
|
|
110
|
+
for (let i = 0; i < memories.length; i++) {
|
|
111
|
+
for (let j = i + 1; j < memories.length; j++) {
|
|
112
|
+
if (memories[i].type !== memories[j].type) continue;
|
|
113
|
+
const sim = cosineSimilarity(vectors[i], vectors[j]);
|
|
114
|
+
if (sim > threshold) pairs.push({ a: memories[i], b: memories[j], similarity: sim });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return pairs;
|
|
118
|
+
}
|