@modusensus/dsh-mneme 0.12.0 → 0.14.0-beta.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 +51 -20
- package/bin/dsh-mneme-choro.mjs +9 -1
- package/docs/CONFIGURATION.md +47 -6
- package/docs/DIRECTORIES.md +114 -0
- package/docs/DISTILL.md +64 -0
- package/docs/DSH-GO.md +97 -0
- package/docs/ENTITIES.md +5 -3
- package/docs/FORGET.md +77 -0
- package/docs/KB.md +110 -0
- package/docs/SLEEP.md +28 -3
- package/docs/STORAGE.md +23 -0
- package/lib/api-standalone.js +4 -1
- package/lib/api.js +788 -30
- package/lib/attachment-text.js +122 -0
- package/lib/choro-command.js +9 -2
- package/lib/client.js +4627 -1409
- package/lib/config.js +166 -1
- package/lib/continuity.js +7 -6
- package/lib/diary/auto.js +190 -0
- package/lib/diary/compose.js +440 -0
- package/lib/diary/dir.js +59 -0
- package/lib/diary/export.js +97 -0
- package/lib/diary/index.js +26 -0
- package/lib/diary/library.js +189 -0
- package/lib/diary/qq-inbox.js +142 -0
- package/lib/diary/rollover.js +61 -0
- package/lib/diary/session-provider.js +122 -0
- package/lib/dream/decisions.js +95 -6
- package/lib/dream/sleep.js +760 -19
- package/lib/dream/triage.js +139 -0
- package/lib/dream.js +168 -21
- package/lib/dsh-go/host.js +162 -0
- package/lib/dsh-go/panel.js +2091 -0
- package/lib/dsh-go/text.js +342 -0
- package/lib/dsh-go/views/common.js +68 -0
- package/lib/dsh-go/views/entity.js +48 -0
- package/lib/dsh-go/views/memory.js +198 -0
- package/lib/dsh-go/views/root.js +30 -0
- package/lib/dsh-go/views/settings.js +398 -0
- package/lib/dsh-go/views/status.js +51 -0
- package/lib/embedding.js +42 -8
- package/lib/entities/extractor.js +36 -7
- package/lib/hot-memory.js +22 -11
- package/lib/index.js +123 -3
- package/lib/inject.js +203 -20
- package/lib/kb/index.js +309 -0
- package/lib/kb/routes.js +220 -0
- package/lib/kb/tools.js +148 -0
- package/lib/lang.js +98 -4
- package/lib/peak-hours.js +57 -2
- package/lib/semantic.js +347 -60
- package/lib/service.js +268 -69
- package/lib/settings.js +128 -15
- package/lib/store.js +585 -26
- package/lib/summarize.js +38 -11
- package/lib/tools.js +35 -4
- package/lib/user-settings.js +128 -32
- package/lib/vector-index.js +76 -6
- package/lib/wiki/claims.js +267 -0
- package/lib/wiki/cli-hint.js +90 -0
- package/lib/wiki/queries.js +282 -2
- package/lib/wiki/render.js +74 -0
- package/lib/wiki/t0.js +43 -6
- package/lib/wiki/t1-jobs.js +232 -0
- package/lib/wiki/t1.js +145 -32
- package/package.json +39 -1
- package/scripts/release-docs.mjs +7 -1
- package/scripts/release-notes.mjs +4 -1
- package/src/api-standalone.js +4 -1
- package/src/api.js +788 -30
- package/src/attachment-text.js +122 -0
- package/src/choro-command.js +9 -2
- package/src/config.js +166 -1
- package/src/continuity.js +7 -6
- package/src/diary/auto.js +190 -0
- package/src/diary/compose.js +440 -0
- package/src/diary/dir.js +59 -0
- package/src/diary/export.js +97 -0
- package/src/diary/index.js +26 -0
- package/src/diary/library.js +189 -0
- package/src/diary/qq-inbox.js +142 -0
- package/src/diary/rollover.js +61 -0
- package/src/diary/session-provider.js +122 -0
- package/src/dream/decisions.js +95 -6
- package/src/dream/sleep.js +760 -19
- package/src/dream/triage.js +139 -0
- package/src/dream.js +168 -21
- package/src/dsh-go/host.js +162 -0
- package/src/dsh-go/panel.js +2091 -0
- package/src/dsh-go/text.js +342 -0
- package/src/dsh-go/views/common.js +68 -0
- package/src/dsh-go/views/entity.js +48 -0
- package/src/dsh-go/views/memory.js +198 -0
- package/src/dsh-go/views/root.js +30 -0
- package/src/dsh-go/views/settings.js +398 -0
- package/src/dsh-go/views/status.js +51 -0
- package/src/embedding.js +42 -8
- package/src/entities/extractor.js +36 -7
- package/src/hot-memory.js +22 -11
- package/src/index.js +123 -3
- package/src/inject.js +203 -20
- package/src/kb/index.js +309 -0
- package/src/kb/routes.js +220 -0
- package/src/kb/tools.js +148 -0
- package/src/lang.js +98 -4
- package/src/peak-hours.js +57 -2
- package/src/semantic.js +347 -60
- package/src/service.js +268 -69
- package/src/settings.js +128 -15
- package/src/store.js +585 -26
- package/src/summarize.js +38 -11
- package/src/tools.js +35 -4
- package/src/user-settings.js +128 -32
- package/src/vector-index.js +76 -6
- package/src/wiki/claims.js +267 -0
- package/src/wiki/cli-hint.js +90 -0
- package/src/wiki/queries.js +282 -2
- package/src/wiki/render.js +74 -0
- package/src/wiki/t0.js +43 -6
- package/src/wiki/t1-jobs.js +232 -0
- package/src/wiki/t1.js +145 -32
- package/test/api-routes-gaps.test.js +63 -0
- package/test/api.test.js +385 -4
- package/test/attachment-text.test.js +140 -0
- package/test/bm25-corpus-cache.test.js +145 -0
- package/test/capacity-warning.test.js +262 -0
- package/test/client.test.js +1051 -185
- package/test/conflict-freeze.test.js +45 -1
- package/test/conflict-ledger-enforce.test.js +452 -0
- package/test/conflict-ledger.test.js +242 -0
- package/test/continuity.test.js +8 -1
- package/test/contrast.test.js +124 -12
- package/test/diary-library.test.js +187 -0
- package/test/diary.test.js +576 -0
- package/test/dream-baseline-clamp.test.js +174 -0
- package/test/dream-maxtokens-fallback.test.js +3 -3
- package/test/dream.test.js +62 -1
- package/test/dsh-go-host.test.js +134 -0
- package/test/dsh-go-panel-actions.test.js +854 -0
- package/test/dsh-go-panel.test.js +251 -0
- package/test/embedding-ready.test.js +33 -3
- package/test/entities.test.js +73 -0
- package/test/forget-corrections.test.js +70 -0
- package/test/graph-api.test.js +124 -0
- package/test/hot-memory.test.js +48 -0
- package/test/image-memory.test.js +171 -0
- package/test/inject-parent-gate.test.js +6 -3
- package/test/inject.test.js +59 -0
- package/test/issue-617-step1-query-and-fusion.test.js +361 -0
- package/test/kb-api-wiring.test.js +128 -0
- package/test/kb-collections.test.js +272 -0
- package/test/kb-routes.test.js +379 -0
- package/test/mint-source.test.js +26 -0
- package/test/panel-chart-scale.test.js +90 -0
- package/test/panel-tokens.test.js +245 -0
- package/test/peak-hours-observability.test.js +70 -0
- package/test/persona-api.test.js +3 -3
- package/test/persona-by-session.test.js +568 -0
- package/test/persona-settings.test.js +7 -7
- package/test/reflection-update-gate.test.js +129 -0
- package/test/reindex-backfill.test.js +334 -0
- package/test/reindex-scheduler.test.js +369 -0
- package/test/release-docs.test.js +20 -0
- package/test/release-notes.test.js +25 -0
- package/test/scope-candidates.test.js +249 -0
- package/test/security-doc.test.js +10 -0
- package/test/sleep-action-set.test.js +14 -0
- package/test/sleep-heat.test.js +49 -0
- package/test/sleep.test.js +635 -0
- package/test/standalone-api.test.js +24 -0
- package/test/store.test.js +20 -0
- package/test/summarize.test.js +26 -0
- package/test/user-settings-segments.test.js +528 -0
- package/test/vector-index.test.js +120 -6
- package/test/wiki-api.test.js +463 -1
- package/test/wiki-claims.test.js +194 -0
- package/test/wiki-cli-hint.test.js +118 -0
- package/test/wiki-provenance.test.js +202 -0
- package/test/wiki-queries.test.js +128 -0
- package/test/wiki-t0.test.js +77 -1
- package/test/wiki-t1-jobs.test.js +125 -0
- package/test/wiki-t1.test.js +226 -1
|
@@ -199,6 +199,255 @@ test("dream (non-freeze): same-scope conflict is still auto-adjudicated (regress
|
|
|
199
199
|
store.close();
|
|
200
200
|
});
|
|
201
201
|
|
|
202
|
+
// Issue #603 第 1 层:freeze 分支此前绕过全部三个上限,且把「已在队列里的对」
|
|
203
|
+
// 当成新停车报进 frozen。下面三条锁住修完之后的三个性质。
|
|
204
|
+
test("#603 freeze: a pair already in the queue is not re-reported as a new freeze", async () => {
|
|
205
|
+
const { store, service, vectorIndex } = setup();
|
|
206
|
+
savePair(service, vectorIndex, "索引策略A", "索引策略B", { agent: "coder" }, { agent: "coder" });
|
|
207
|
+
const logger = { warn: () => {}, info: () => {} };
|
|
208
|
+
|
|
209
|
+
const first = await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), logger, { embedder, vectorIndex }, null);
|
|
210
|
+
assert.equal(first.phases.conflicts.frozen, 1, "第一次是真插入");
|
|
211
|
+
assert.equal(store.countConflictPending(), 1);
|
|
212
|
+
|
|
213
|
+
const second = await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), logger, { embedder, vectorIndex }, null);
|
|
214
|
+
assert.equal(second.phases.conflicts.frozen, 0, "第二次检出的是已入队的对,不算新停车");
|
|
215
|
+
assert.equal(second.status, "noop", "没有新停车就是 noop,不是 ok");
|
|
216
|
+
assert.equal(store.countConflictPending(), 1, "队列没有被重复写入");
|
|
217
|
+
store.close();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test("#603 freeze: cap is applied after filtering already-parked pairs, so the queue keeps growing", async () => {
|
|
221
|
+
const { store, service, vectorIndex } = setup();
|
|
222
|
+
// 三对互不相交的高相似记忆:贪心配对每轮都能检出三对。
|
|
223
|
+
savePair(service, vectorIndex, "队列A1", "队列A2", { agent: "coder" }, { agent: "coder" });
|
|
224
|
+
savePair(service, vectorIndex, "队列B1", "队列B2", { agent: "coder" }, { agent: "coder" });
|
|
225
|
+
savePair(service, vectorIndex, "队列C1", "队列C2", { agent: "coder" }, { agent: "coder" });
|
|
226
|
+
const logger = { warn: () => {}, info: () => {} };
|
|
227
|
+
// 每轮只处理一对,且远小于候选数——裸 slice 会让第一对永远占住这个名额。
|
|
228
|
+
const cfg = baseConfig({ conflictFreezeEnabled: true, sleepConflictMaxPairsPerRun: 1 });
|
|
229
|
+
|
|
230
|
+
await runSleep(llmCtx(), service, cfg, logger, { embedder, vectorIndex }, null);
|
|
231
|
+
assert.equal(store.countConflictPending(), 1);
|
|
232
|
+
await runSleep(llmCtx(), service, cfg, logger, { embedder, vectorIndex }, null);
|
|
233
|
+
assert.equal(store.countConflictPending(), 2, "第二对必须能轮到,不能被第一对饿死");
|
|
234
|
+
await runSleep(llmCtx(), service, cfg, logger, { embedder, vectorIndex }, null);
|
|
235
|
+
assert.equal(store.countConflictPending(), 3, "第三对同理");
|
|
236
|
+
|
|
237
|
+
const done = await runSleep(llmCtx(), service, cfg, logger, { embedder, vectorIndex }, null);
|
|
238
|
+
assert.equal(done.phases.conflicts.frozen, 0, "全部入队后不再虚报");
|
|
239
|
+
assert.equal(done.phases.conflicts.perRunSkipped, 0, "没有新对可处理,不算被截断");
|
|
240
|
+
store.close();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
test("#603 freeze: the pending queue budget stops parking and says so", async () => {
|
|
244
|
+
const { store, service, vectorIndex } = setup();
|
|
245
|
+
savePair(service, vectorIndex, "预算A1", "预算A2", { agent: "coder" }, { agent: "coder" });
|
|
246
|
+
savePair(service, vectorIndex, "预算B1", "预算B2", { agent: "coder" }, { agent: "coder" });
|
|
247
|
+
savePair(service, vectorIndex, "预算C1", "预算C2", { agent: "coder" }, { agent: "coder" });
|
|
248
|
+
const warnings = [];
|
|
249
|
+
const logger = { warn: (m) => warnings.push(String(m)), info: () => {} };
|
|
250
|
+
|
|
251
|
+
const result = await runSleep(
|
|
252
|
+
llmCtx(), service,
|
|
253
|
+
baseConfig({ conflictFreezeEnabled: true, conflictFreezeMaxPending: 2 }),
|
|
254
|
+
logger, { embedder, vectorIndex }, null
|
|
255
|
+
);
|
|
256
|
+
assert.equal(store.countConflictPending(), 2, "只装到队列上限为止");
|
|
257
|
+
assert.equal(result.phases.conflicts.frozen, 2);
|
|
258
|
+
assert.equal(result.phases.conflicts.pendingSkipped, 1, "被队列上限挡下的对要如实报出来");
|
|
259
|
+
assert.ok(warnings.some((w) => /freeze queue full/.test(w)), "要留一条 warn,不能静默丢弃");
|
|
260
|
+
|
|
261
|
+
// 队列已满:再跑一轮不会新增,也不会把「满了」报成可续排的 backlog。
|
|
262
|
+
const again = await runSleep(
|
|
263
|
+
llmCtx(), service,
|
|
264
|
+
baseConfig({ conflictFreezeEnabled: true, conflictFreezeMaxPending: 2 }),
|
|
265
|
+
logger, { embedder, vectorIndex }, null
|
|
266
|
+
);
|
|
267
|
+
assert.equal(store.countConflictPending(), 2);
|
|
268
|
+
assert.equal(again.phases.conflicts.frozen, 0);
|
|
269
|
+
assert.equal(again.phases.conflicts.perRunSkipped, 0, "队列满不是 per-run 截断,不该触发续排");
|
|
270
|
+
store.close();
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
// Issue #603 第 2 层:sleep 停车时必须把候选对的相似度一起落库。此前这个数字只
|
|
274
|
+
// 活在 reason 文本里(「相似度 0.87」),面板靠正则回收、排序无从下手——同一轮
|
|
275
|
+
// 产出的对 created_at 全同,队列顺序等于随机。
|
|
276
|
+
test("#603 freeze: the parked row carries the similarity the detector measured", async () => {
|
|
277
|
+
const { store, service, vectorIndex } = setup();
|
|
278
|
+
savePair(service, vectorIndex, "排序A1", "排序A2", { agent: "coder" }, { agent: "coder" });
|
|
279
|
+
|
|
280
|
+
await runSleep(llmCtx(), service, baseConfig({ conflictFreezeEnabled: true }), { warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null);
|
|
281
|
+
|
|
282
|
+
const [row] = store.listConflictPending();
|
|
283
|
+
assert.ok(row, "the pair must be parked");
|
|
284
|
+
// 两条记忆的向量都是 [1,0,0],余弦为 1。断言「是数字且等于 1」而不是「非空」:
|
|
285
|
+
// 前者能抓住「传了 undefined / 传了字符串」这类静默降级。
|
|
286
|
+
assert.equal(typeof row.similarity, "number", "similarity must be a number, not a string or undefined");
|
|
287
|
+
assert.equal(row.similarity, 1, "identical vectors are similarity 1");
|
|
288
|
+
store.close();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
// 与上一条互补:拿不到相似度时必须是「未知」而不是 0。0 会被排到最后,等于替
|
|
292
|
+
// 用户断言「这两条毫不相似」——那是编造。
|
|
293
|
+
test("#603 freeze: a pair parked without a measured similarity stays unknown", async () => {
|
|
294
|
+
const { store, service } = setup();
|
|
295
|
+
// 直接走 service 的停车口(没有向量索引参与 = 没测过相似度)。
|
|
296
|
+
const a = service.saveWithDedupe({ type: "project", title: "未知A", content: "正文A" }).memory;
|
|
297
|
+
const b = service.saveWithDedupe({ type: "project", title: "未知B", content: "正文B" }).memory;
|
|
298
|
+
service.saveConflictPending({ run_id: "r1", memory_a: a.id, memory_b: b.id, reason: "人工停车" });
|
|
299
|
+
|
|
300
|
+
const [row] = store.listConflictPending();
|
|
301
|
+
assert.equal(row.similarity, undefined, "no measurement must read back as unknown, not 0");
|
|
302
|
+
store.close();
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
// Issue #603 第 4 层:自动裁决档。相似度极高的**同 scope** 对直接保留新条、归档
|
|
306
|
+
// 旧条,不进人工队列;跨 scope 对永不自动(归属是用户的决定)。
|
|
307
|
+
test("#603 auto-resolve: a near-duplicate same-scope pair is superseded without entering the queue", async () => {
|
|
308
|
+
const { store, service, vectorIndex } = setup();
|
|
309
|
+
const [first, second] = savePair(service, vectorIndex, "缓存策略A", "缓存策略B", { agent: "coder" }, { agent: "coder" });
|
|
310
|
+
|
|
311
|
+
const result = await runSleep(
|
|
312
|
+
llmCtx(), service,
|
|
313
|
+
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
314
|
+
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
315
|
+
);
|
|
316
|
+
assert.equal(result.status, "ok");
|
|
317
|
+
|
|
318
|
+
// 队列必须是空的:这一对根本没进人工队列。
|
|
319
|
+
assert.equal(store.countConflictPending(), 0, "auto-resolved pairs must not reach the manual queue");
|
|
320
|
+
// 不猜 id 顺序,用归档状态反推哪一侧被取代。
|
|
321
|
+
const archivedIds = [first.id, second.id].filter((id) => store.getById(id).archived);
|
|
322
|
+
assert.equal(archivedIds.length, 1, "exactly one side is archived");
|
|
323
|
+
const loser = archivedIds[0];
|
|
324
|
+
const survivor = [first.id, second.id].find((id) => !store.getById(id).archived);
|
|
325
|
+
// 留下的那条正文一字不动(supersede 把注记写在败者侧,不污染幸存者)。
|
|
326
|
+
assert.doesNotMatch(store.getById(survivor).content, /已被取代|superseded by/,
|
|
327
|
+
"the survivor's body must stay clean");
|
|
328
|
+
// 败者侧带可追溯注记(#126 的原设计)。
|
|
329
|
+
assert.match(store.getById(loser).content, /已被取代|superseded by/,
|
|
330
|
+
"traceability lives on the archived side");
|
|
331
|
+
// 机器台账也要有一条:自动动作必须能事后回答「什么时候、被谁判的」。
|
|
332
|
+
const reviewed = typeof store.listConflictReviewed === "function" ? store.listConflictReviewed({ limit: 10 }) : [];
|
|
333
|
+
assert.ok(reviewed.length >= 1, "auto-resolve must leave a ledger row");
|
|
334
|
+
assert.equal(reviewed[0].outcome, "supersede");
|
|
335
|
+
store.close();
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test("#603 auto-resolve: a cross-scope pair is never auto-adjudicated, even at similarity 1", async () => {
|
|
339
|
+
const { store, service, vectorIndex } = setup();
|
|
340
|
+
const [xNew, xOld] = savePair(service, vectorIndex, "部署清单A", "部署清单B", { agent: "coder" }, { agent: "writer" });
|
|
341
|
+
|
|
342
|
+
const result = await runSleep(
|
|
343
|
+
llmCtx(), service,
|
|
344
|
+
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
345
|
+
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
346
|
+
);
|
|
347
|
+
assert.equal(result.status, "ok");
|
|
348
|
+
|
|
349
|
+
// 向量完全相同(余弦 1),但归属不同 → 只能停车等人判。
|
|
350
|
+
assert.equal(store.countConflictPending(), 1, "cross-scope pair still goes to the manual queue");
|
|
351
|
+
assert.equal(store.getById(xOld.id).archived, false, "cross-scope loser is never auto-archived");
|
|
352
|
+
assert.equal(store.getById(xNew.id).archived, false, "cross-scope winner is untouched too");
|
|
353
|
+
store.close();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
test("#603 auto-resolve: with the switch off nothing is auto-adjudicated", async () => {
|
|
357
|
+
const { store, service, vectorIndex } = setup();
|
|
358
|
+
savePair(service, vectorIndex, "主题A", "主题B", { agent: "coder" }, { agent: "coder" });
|
|
359
|
+
|
|
360
|
+
const result = await runSleep(
|
|
361
|
+
llmCtx(), service,
|
|
362
|
+
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: false }),
|
|
363
|
+
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
364
|
+
);
|
|
365
|
+
assert.equal(result.status, "ok");
|
|
366
|
+
assert.equal(store.countConflictPending(), 1, "with the switch off, nothing is auto-adjudicated");
|
|
367
|
+
store.close();
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
// 自动裁决保留的是**信息量更大**的那条,不是更新的那条。
|
|
371
|
+
// 反例来自 live 库:>=0.95 的 20 对里 11 对(55%)新条比旧条短,平均短 132 字——
|
|
372
|
+
// 蒸馏压缩过的一版反而更新时间更晚,按「留新的」会把更完整的那条归档掉。
|
|
373
|
+
test("#603 auto-resolve: the longer copy survives even when the shorter one is newer", async () => {
|
|
374
|
+
const { store, service, vectorIndex } = setup();
|
|
375
|
+
// 先存长的那条(更旧),再存短的那条(更新):新条更短,正是实测里的多数情况。
|
|
376
|
+
const longBody = "缓存策略要按写入频率分三档:热数据走内存、温数据走 SSD、冷数据归档到对象存储。"
|
|
377
|
+
+ "切换阈值由每小时访问次数决定,低于一次就降档,连续三天高于十次再升档。";
|
|
378
|
+
const shortBody = "缓存策略按写入频率分档。";
|
|
379
|
+
const older = service.saveWithDedupe({
|
|
380
|
+
type: "project", title: "缓存分层", content: longBody
|
|
381
|
+
}).memory;
|
|
382
|
+
const newer = service.saveWithDedupe({
|
|
383
|
+
type: "project", title: "缓存分层摘要", content: shortBody
|
|
384
|
+
}).memory;
|
|
385
|
+
vectorIndex.saveEmbedding(older.id, [1, 0, 0]);
|
|
386
|
+
vectorIndex.saveEmbedding(newer.id, [1, 0, 0]);
|
|
387
|
+
assert.ok(older.content.length > newer.content.length, "fixture: the older copy must be the longer one");
|
|
388
|
+
|
|
389
|
+
const result = await runSleep(
|
|
390
|
+
llmCtx(), service,
|
|
391
|
+
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
392
|
+
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
393
|
+
);
|
|
394
|
+
assert.equal(result.status, "ok");
|
|
395
|
+
assert.equal(result.phases.conflicts.autoResolved, 1, "the pair is auto-adjudicated");
|
|
396
|
+
|
|
397
|
+
// 幸存者必须是长的那条:留新的会把三档策略削成一句话。
|
|
398
|
+
assert.equal(store.getById(older.id).archived, false, "the longer, older copy must survive");
|
|
399
|
+
assert.equal(store.getById(newer.id).archived, true, "the shorter, newer copy is the one archived");
|
|
400
|
+
store.close();
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// 长度相同才是「信息量相当」,此时退回更新时间:新条胜出,且结论确定可重放。
|
|
404
|
+
test("#603 auto-resolve: equal-length copies fall back to the newer one, deterministically", async () => {
|
|
405
|
+
const { store, service, vectorIndex } = setup();
|
|
406
|
+
const body = "部署脚本先跑 migrate 再跑 smoke,任一步失败就回滚到上一个 tag。";
|
|
407
|
+
const older = service.saveWithDedupe({ type: "project", title: "部署甲", content: body }).memory;
|
|
408
|
+
const newer = service.saveWithDedupe({ type: "project", title: "部署乙", content: body }).memory;
|
|
409
|
+
vectorIndex.saveEmbedding(older.id, [1, 0, 0]);
|
|
410
|
+
vectorIndex.saveEmbedding(newer.id, [1, 0, 0]);
|
|
411
|
+
|
|
412
|
+
const result = await runSleep(
|
|
413
|
+
llmCtx(), service,
|
|
414
|
+
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
415
|
+
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
416
|
+
);
|
|
417
|
+
assert.equal(result.status, "ok");
|
|
418
|
+
assert.equal(result.phases.conflicts.autoResolved, 1);
|
|
419
|
+
// 等长 → 比 updated_at:后存的赢。长度相等时不至于两边都"更完整",留新的是安全的。
|
|
420
|
+
assert.equal(store.getById(newer.id).archived, false, "equal length: the newer copy survives");
|
|
421
|
+
assert.equal(store.getById(older.id).archived, true);
|
|
422
|
+
store.close();
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("#603 auto-resolve: a failed adjudication falls back to the manual queue instead of vanishing", async () => {
|
|
426
|
+
const { store, service, vectorIndex } = setup();
|
|
427
|
+
savePair(service, vectorIndex, "回退A", "回退B", { agent: "coder" }, { agent: "coder" });
|
|
428
|
+
|
|
429
|
+
// 让 supersede 一定失败:把 applyDecisions 依赖的 update 打断。
|
|
430
|
+
const realUpdate = service.update.bind(service);
|
|
431
|
+
let calls = 0;
|
|
432
|
+
service.update = (...args) => {
|
|
433
|
+
calls++;
|
|
434
|
+
throw new Error("write refused");
|
|
435
|
+
};
|
|
436
|
+
|
|
437
|
+
const result = await runSleep(
|
|
438
|
+
llmCtx(), service,
|
|
439
|
+
baseConfig({ conflictFreezeEnabled: true, conflictAutoResolveEnabled: true, conflictAutoResolveMinSimilarity: 0.95 }),
|
|
440
|
+
{ warn: () => {}, info: () => {} }, { embedder, vectorIndex }, null
|
|
441
|
+
);
|
|
442
|
+
service.update = realUpdate;
|
|
443
|
+
|
|
444
|
+
assert.ok(calls > 0, "the auto path did try to apply");
|
|
445
|
+
// 自动档没落地 → 这一对必须退回人工队列。丢掉它才是真正的静默丢失。
|
|
446
|
+
assert.equal(store.countConflictPending(), 1, "a failed auto-resolve must land in the manual queue");
|
|
447
|
+
assert.equal(result.status, "ok", "the run itself is not a failure");
|
|
448
|
+
store.close();
|
|
449
|
+
});
|
|
450
|
+
|
|
202
451
|
test("non-freeze with an LLM: cross-scope pair bypasses adjudication; same-scope pair is adjudicated", async () => {
|
|
203
452
|
const { store, service, vectorIndex } = setup();
|
|
204
453
|
const [yNew, yOld] = savePair(service, vectorIndex, "缓存策略A", "缓存策略B", { agent: "coder" }, { agent: "coder" });
|
|
@@ -34,12 +34,22 @@ function section(heading) {
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
test("SECURITY.md 的支持版本表跟着 package.json 走(中英各一份)", () => {
|
|
37
|
+
// 预发布版(0.14.0-beta.1)不占支持表的格:beta 不是受支持的稳定版本,把首行挪成
|
|
38
|
+
// 0.14.x 等于对外宣称 0.14 已在活跃维护。这里改成「首行必须不是 beta 的 minor」——
|
|
39
|
+
// 仍是断言,不是跳过:真有人给 beta 挪了格,这条会红。
|
|
40
|
+
const isPrerelease = version.includes("-");
|
|
37
41
|
for (const heading of ["## Supported Versions", "## 支持版本"]) {
|
|
38
42
|
const body = section(heading);
|
|
39
43
|
// 表第一行就是「当前支持」的那一行;锁它,别锁整表(历史版本行本来就该在)。
|
|
40
44
|
const rows = body.split(/\r?\n/).filter((l) => /^\|\s*[\d<]/.test(l));
|
|
41
45
|
assert.ok(rows.length >= 2, `${heading} 的版本表应至少两行,实际 ${rows.length} 行`);
|
|
42
46
|
const current = rows[0].split("|")[1].trim();
|
|
47
|
+
if (isPrerelease) {
|
|
48
|
+
assert.notEqual(current, expectedVersionRow,
|
|
49
|
+
`${heading} 的首行不该是 ${expectedVersionRow}(package.json 为预发布版 ${version})——`
|
|
50
|
+
+ "预发布版占格等于宣称它已在活跃维护");
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
43
53
|
assert.equal(current, expectedVersionRow,
|
|
44
54
|
`${heading} 的首行应写 ${expectedVersionRow}(package.json 为 ${version}),实际写了 ${current}`);
|
|
45
55
|
}
|
|
@@ -373,6 +373,20 @@ test("issue#126r: buildOutcome records a disposition for the new actions", () =>
|
|
|
373
373
|
assert.equal(outcome.byId.d2, "differentiated");
|
|
374
374
|
});
|
|
375
375
|
|
|
376
|
+
// #619 / Ephor 第七轮 ②:dream 那条路上,一个「只刷新不铸造」的轮此前 outcome.byId
|
|
377
|
+
// 是空对象——审计行看不出任何目标被处置过。#126 review 在 supersede 上踩过同一个
|
|
378
|
+
// 坑(注释就写在 buildOutcome 里),这次 create_merged 又漏了。刷新要有自己的名字,
|
|
379
|
+
// 不能冒充 created:一个是「新生了这条」,一个是「把既有那条重写了」。
|
|
380
|
+
test("issue#619r: buildOutcome names the create dispositions, refresh is not a birth", () => {
|
|
381
|
+
const outcome = buildOutcome([
|
|
382
|
+
{ action: "create", id: "fresh-1", type: "pattern" },
|
|
383
|
+
{ action: "create_merged", id: "42b75761", type: "pattern" }
|
|
384
|
+
]);
|
|
385
|
+
assert.equal(outcome.byId["fresh-1"], "created");
|
|
386
|
+
assert.equal(outcome.byId["42b75761"], "created-refreshed", "刷新不是 created");
|
|
387
|
+
assert.deepEqual(outcome.byId, { "fresh-1": "created", "42b75761": "created-refreshed" });
|
|
388
|
+
});
|
|
389
|
+
|
|
376
390
|
test("issue#533r: differentiate leaves the cached vectors alone (nothing changed to re-embed)", async () => {
|
|
377
391
|
const { service, store, vectorIndex } = setup();
|
|
378
392
|
const { a, b } = seedPair(service, vectorIndex, "向量甲", "向量乙");
|
package/test/sleep-heat.test.js
CHANGED
|
@@ -124,3 +124,52 @@ test("demote tier (30-90d) fires when the type's λ is cold enough", async () =>
|
|
|
124
124
|
assert.ok(after._full_content, "全文停放在 _full_content");
|
|
125
125
|
store.close();
|
|
126
126
|
});
|
|
127
|
+
|
|
128
|
+
// Issue #603 台账虚报:phaseDemotion 原来不看 demoteToSummary 的返回值、无条件
|
|
129
|
+
// push,于是「已经降级过的行」每轮都被选中、空转一次事务、再被记成降级了一次。
|
|
130
|
+
// live 库实测 12 轮里虚报 43%–99%(有一轮报 81 条、81 条全是重复)。这里钉住两件事:
|
|
131
|
+
// 1. 已降级的行不再进 demoted(台账不再虚报);
|
|
132
|
+
// 2. 它也不再被重复处理(省掉那 138 次空事务)。
|
|
133
|
+
test("#603 demotion: an already-demoted memory is not reported as demoted again", async () => {
|
|
134
|
+
const { store, service } = setup();
|
|
135
|
+
const mem = saveMemory(service, "旧会话记录", "可压缩的历史内容", "history", 2);
|
|
136
|
+
const config = sleepConfig({ heatTypeDecay: { history: 0.05 } });
|
|
137
|
+
|
|
138
|
+
// 第一轮:真的降级一次。
|
|
139
|
+
const first = await run([[mem, 40]], service, config);
|
|
140
|
+
assert.ok(first.demoted.includes(mem.id), "the first run really demotes it");
|
|
141
|
+
assert.ok(store.getById(mem.id)._full_content, "full text moved to _full_content");
|
|
142
|
+
|
|
143
|
+
// 第二轮:同一行已经降级过 → 台账里不能再出现它。
|
|
144
|
+
const second = await run([[mem, 40]], service, config);
|
|
145
|
+
assert.equal(second.demoted.includes(mem.id), false,
|
|
146
|
+
"an already-demoted row must not be reported as demoted again");
|
|
147
|
+
assert.deepEqual(second.demoted, [], "the ledger must be empty, not a replay of the same row");
|
|
148
|
+
assert.equal(second.status, "noop", "nothing happened, so the phase is a noop");
|
|
149
|
+
store.close();
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// 与上一条互补:被 demoteToSummary 的 minRefTimeMs 挡下(快照之后被 recall 摸热)
|
|
153
|
+
// 也返回 undefined。这种「判定该降、实际没动」必须单独报出来,不能混进 demoted。
|
|
154
|
+
test("#603 demotion: a row the freshness guard spares is counted as skipped, not demoted", async () => {
|
|
155
|
+
const { store, service } = setup();
|
|
156
|
+
const mem = saveMemory(service, "刚被摸过的记录", "正文内容", "history", 2);
|
|
157
|
+
const config = sleepConfig({ heatTypeDecay: { history: 0.05 } });
|
|
158
|
+
|
|
159
|
+
// 把 demoteToSummary 包一层,模拟「事务内发现这行刚被 recall 摸热」→ 返回 undefined。
|
|
160
|
+
const realDemote = service.demoteToSummary.bind(service);
|
|
161
|
+
let touched = 0;
|
|
162
|
+
service.demoteToSummary = (...args) => {
|
|
163
|
+
touched++;
|
|
164
|
+
return undefined;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const demotion = await run([[mem, 40]], service, config);
|
|
168
|
+
service.demoteToSummary = realDemote;
|
|
169
|
+
|
|
170
|
+
assert.ok(touched > 0, "the demote path was attempted");
|
|
171
|
+
assert.equal(demotion.demoted.includes(mem.id), false, "nothing landed, so nothing is reported as demoted");
|
|
172
|
+
assert.equal(demotion.demoteSkipped, 1, "the spared row is reported separately");
|
|
173
|
+
assert.equal(demotion.status, "noop", "no work landed");
|
|
174
|
+
store.close();
|
|
175
|
+
});
|