@modusensus/dsh-mneme 0.13.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 +6 -4
- package/bin/dsh-mneme-choro.mjs +9 -1
- package/docs/CONFIGURATION.md +5 -3
- package/lib/api.js +211 -5
- package/lib/client.js +1240 -354
- package/lib/config.js +27 -0
- package/lib/dream/decisions.js +39 -1
- package/lib/dream/sleep.js +146 -6
- package/lib/dream/triage.js +139 -0
- package/lib/dream.js +123 -8
- package/lib/hot-memory.js +14 -2
- package/lib/index.js +31 -1
- package/lib/inject.js +14 -2
- package/lib/lang.js +11 -0
- package/lib/service.js +76 -4
- package/lib/settings.js +16 -1
- package/lib/store.js +238 -5
- package/lib/wiki/claims.js +267 -0
- package/lib/wiki/queries.js +168 -2
- package/lib/wiki/render.js +74 -0
- package/lib/wiki/t0.js +43 -6
- package/lib/wiki/t1.js +138 -31
- package/package.json +15 -1
- package/scripts/release-docs.mjs +7 -1
- package/scripts/release-notes.mjs +4 -1
- package/src/api.js +211 -5
- package/src/config.js +27 -0
- package/src/dream/decisions.js +39 -1
- package/src/dream/sleep.js +146 -6
- package/src/dream/triage.js +139 -0
- package/src/dream.js +123 -8
- package/src/hot-memory.js +14 -2
- package/src/index.js +31 -1
- package/src/inject.js +14 -2
- package/src/lang.js +11 -0
- package/src/service.js +76 -4
- package/src/settings.js +16 -1
- package/src/store.js +238 -5
- package/src/wiki/claims.js +267 -0
- package/src/wiki/queries.js +168 -2
- package/src/wiki/render.js +74 -0
- package/src/wiki/t0.js +43 -6
- package/src/wiki/t1.js +138 -31
- package/test/api.test.js +11 -1
- package/test/client.test.js +275 -34
- package/test/conflict-ledger-enforce.test.js +452 -0
- package/test/conflict-ledger.test.js +242 -0
- package/test/contrast.test.js +124 -12
- package/test/dream-baseline-clamp.test.js +174 -0
- package/test/graph-api.test.js +124 -0
- package/test/hot-memory.test.js +48 -0
- package/test/inject-parent-gate.test.js +6 -3
- package/test/inject.test.js +59 -0
- package/test/panel-chart-scale.test.js +90 -0
- package/test/panel-tokens.test.js +245 -0
- package/test/release-docs.test.js +20 -0
- package/test/release-notes.test.js +25 -0
- package/test/security-doc.test.js +10 -0
- package/test/sleep.test.js +37 -0
- package/test/wiki-api.test.js +110 -3
- package/test/wiki-claims.test.js +194 -0
- package/test/wiki-provenance.test.js +202 -0
- package/test/wiki-queries.test.js +121 -0
- package/test/wiki-t0.test.js +77 -1
- package/test/wiki-t1.test.js +226 -1
package/src/config.js
CHANGED
|
@@ -595,6 +595,33 @@ export const Config = z.object({
|
|
|
595
595
|
conflictAutoResolveEnabled: z.boolean().default(false),
|
|
596
596
|
conflictAutoResolveMinSimilarity: z.number().min(0.85).max(1).default(0.95),
|
|
597
597
|
|
|
598
|
+
// --- conflict ledger(issue #641,beta)-----------------------------------
|
|
599
|
+
// 冲突分诊账本的档位。真实库 788 对人审队列的全量分类:46% 近重复 + 20% 跨作用域
|
|
600
|
+
// 重复 + 5% 时序演进,真矛盾只有 ~27%——现行窄 prompt 把前 71% 全逼成了「输赢」,
|
|
601
|
+
// 再由人逐对裁决(≈30 条/天,且 92% 的对从未两侧同场)。分诊把「谁该机械处理、
|
|
602
|
+
// 谁才是真矛盾」从 LLM 手里拿回来(E13b:winner-take-all 裁决 P(毒胜出)=86.8%)。
|
|
603
|
+
//
|
|
604
|
+
// "off" 默认。不分类、不记账,conflict 相位与本键引入前逐字节一致。
|
|
605
|
+
// "shadow" 只分类 + 落 conflict_ledger 账本(bookkeeping,不触发 write hooks),
|
|
606
|
+
// freeze/LLM 路径零变化——beta 实验的测量期:shadow 一周拿真实分诊
|
|
607
|
+
// 分布,验证是否复现 46/20/5/27。
|
|
608
|
+
// "enforce" 确定性处置:T1(dup)走信息量优先 supersede(supersedeInfoFirstPair,
|
|
609
|
+
// 败者归档、胜者正文不动、可追溯注记在被归档侧);T2(跨作用域重复)
|
|
610
|
+
// 与 T4(真矛盾)只记账——不停车、不送 LLM、不动正文;T3(时序演进)
|
|
611
|
+
// 落取代链:旧行 superseded_by/superseded_at 指向新行,两行都留着、
|
|
612
|
+
// 正文都不动,检索侧按降权处理。人审队列与 LLM 裁决都不再消费冲突
|
|
613
|
+
// 候选;与 conflictFreezeEnabled 同开时 freeze 让位(降级 shadow
|
|
614
|
+
// + warn,#616 父子绑定不继承)。
|
|
615
|
+
//
|
|
616
|
+
// 判据唯一实现在 src/dream/triage.js;处置共享实现在 decisions.js 的
|
|
617
|
+
// supersedeInfoFirstPair。每个档位的行为都真实存在——#616 的「假成功」教训
|
|
618
|
+
// (autoMin 0.95 在现库覆盖为 0,开了等于没开)。
|
|
619
|
+
conflictLedgerMode: z.union([z.const("off"), z.const("shadow"), z.const("enforce")]).default("off"),
|
|
620
|
+
// T1(dup)档的相似度下限。检测阈值(normal 0.85)与自动档 autoMin(0.95)之间
|
|
621
|
+
// 的空带由它接管:真实库 reason 相似度回声集中在 0.85-0.93,正好落在两个阈值
|
|
622
|
+
// 之间、自动档开了也接不住的那一段。下界与检测阈值同源。
|
|
623
|
+
conflictLedgerDupMinSimilarity: z.number().min(0.85).max(1).default(0.9),
|
|
624
|
+
|
|
598
625
|
// --- entity gene (v0.3.0) -----------------------------------------------
|
|
599
626
|
// Opt-in: when false (default) nothing in the pipeline extracts entities.
|
|
600
627
|
// The storage layer (entities/entity_attrs/entity_relations tables + CRUD)
|
package/src/dream/decisions.js
CHANGED
|
@@ -11,7 +11,7 @@ export const ACTIONS = new Set(["keep", "merge", "archive", "conflict", "update"
|
|
|
11
11
|
// Epistemic trust (v0.4.5): when config.trustEpistemicWeighting is on, merge
|
|
12
12
|
// keepSource and conflict winners prefer the higher-trust memory. Higher value
|
|
13
13
|
// = preferred. observation (measured) > inferred (derived) > subjective (guess).
|
|
14
|
-
const EPISTEMIC_PRIORITY = { observation: 3, inferred: 2, subjective: 1 };
|
|
14
|
+
export const EPISTEMIC_PRIORITY = { observation: 3, inferred: 2, subjective: 1 };
|
|
15
15
|
|
|
16
16
|
// Issue #104 方向 2:archive 类型护栏的长保留类型集合——这些类型的存在意义
|
|
17
17
|
// 就是长期保存(踩坑记录、约束、用户偏好、挖掘出的模式),LLM 以「价值判断」
|
|
@@ -766,6 +766,44 @@ function applyConflict(d, service, snapshot, config = {}) {
|
|
|
766
766
|
return { applied: 1, committed: { action: "conflict", winner: d.winner, loser: d.loser, count_before: 2, count_after: 1 } };
|
|
767
767
|
}
|
|
768
768
|
|
|
769
|
+
/**
|
|
770
|
+
* 确定性信息量优先 supersede(issue #641 enforce 档的 T1 处置,供 sleep/dream
|
|
771
|
+
* 两条链路共用)。与冻结分支自动裁决档(#603 第 4 层,sleep.js 内联实现)同一
|
|
772
|
+
* 条契约,本函数是它的共享出口:
|
|
773
|
+
* · 保留**信息量更大**的那条,不是更新的——#616 实测 55-57% 的新条反而更短
|
|
774
|
+
* (蒸馏压缩过的一版常常更新),留新条等于把更完整的那条归档掉;
|
|
775
|
+
* · 长度相同才退回 updated_at,再退回 id——三级兜底保证同一对每次判出同一个
|
|
776
|
+
* 赢家(重放不翻面);
|
|
777
|
+
* · trustEpistemicWeighting 显式关:自动处置的承诺必须是确定的,可信度换位
|
|
778
|
+
* 不让结果随 epistemic_status 数据漂移;
|
|
779
|
+
* · 赢家正文一字不动——supersede 的可追溯注记落在被归档侧(#126 原设计)。
|
|
780
|
+
* E13b 证明 conflict 动作的「败者前 100 字追加进赢家」是值级泄漏通道,
|
|
781
|
+
* 确定性处置一律走 supersede、不走 conflict。
|
|
782
|
+
* CAS 快照 + 事务 + 幂等(败者已归档 → applied 0)全部沿用 applyDecisions,
|
|
783
|
+
* 不另写第二份裁决路径。
|
|
784
|
+
*
|
|
785
|
+
* @param {{a: object, b: object}} p 候选对(完整记忆行:id/title/content/updated_at)
|
|
786
|
+
* @returns {{applied: boolean, winnerId: string, loserId: string, winner: object, loser: object}}
|
|
787
|
+
*/
|
|
788
|
+
export function supersedeInfoFirstPair(p, service, logger = null, config = {}) {
|
|
789
|
+
const an = p.a.content?.length ?? 0;
|
|
790
|
+
const bn = p.b.content?.length ?? 0;
|
|
791
|
+
const [winner, loser] = an !== bn
|
|
792
|
+
? (an > bn ? [p.a, p.b] : [p.b, p.a])
|
|
793
|
+
: (p.a.updated_at ?? "") !== (p.b.updated_at ?? "")
|
|
794
|
+
? ((p.a.updated_at ?? "") > (p.b.updated_at ?? "") ? [p.a, p.b] : [p.b, p.a])
|
|
795
|
+
: (p.a.id > p.b.id ? [p.a, p.b] : [p.b, p.a]);
|
|
796
|
+
const snapshot = new Map();
|
|
797
|
+
for (const m of [winner, loser]) {
|
|
798
|
+
snapshot.set(m.id, { updated_at: m.updated_at, content: m.content, title: m.title });
|
|
799
|
+
}
|
|
800
|
+
const { applied } = applyDecisions(
|
|
801
|
+
[{ action: "supersede", winner: winner.id, loser: loser.id }],
|
|
802
|
+
service, logger, snapshot, { ...config, trustEpistemicWeighting: false }
|
|
803
|
+
);
|
|
804
|
+
return { applied: applied > 0, winnerId: winner.id, loserId: loser.id, winner, loser };
|
|
805
|
+
}
|
|
806
|
+
|
|
769
807
|
/**
|
|
770
808
|
* supersede(Issue #126,sleepActionSet="full"):演进型重复——新版本取代旧版本。
|
|
771
809
|
* 与 conflict 的关键差别:**注记追加到 loser(被取代方)而不是 winner**。conflict
|
package/src/dream/sleep.js
CHANGED
|
@@ -23,6 +23,8 @@ import { STR, langOf } from "../lang.js";
|
|
|
23
23
|
import { randomUUID, createHash } from "node:crypto";
|
|
24
24
|
import { validateDecisions, applyDecisions, ACTIONS } from "./decisions.js";
|
|
25
25
|
import { findPotentialConflicts, cosineSimilarity } from "./clustering.js";
|
|
26
|
+
import { triagePair, recordLedgerTriage } from "./triage.js";
|
|
27
|
+
import { supersedeInfoFirstPair } from "./decisions.js";
|
|
26
28
|
import { scopeKeyOf } from "../scope.js";
|
|
27
29
|
import { buildReceipt, describeStreamFailure, resolveDreamEffort, withEffortFallback, withMaxTokensFallback, maintainIndexAfterDream, fenceListBody, parseFailureMeta } from "../dream.js";
|
|
28
30
|
import { computeHeat } from "../heat.js";
|
|
@@ -376,7 +378,18 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
376
378
|
if (!rec?.judged_at) return false;
|
|
377
379
|
return (p.a.updated_at ?? "") <= rec.judged_at && (p.b.updated_at ?? "") <= rec.judged_at;
|
|
378
380
|
};
|
|
379
|
-
|
|
381
|
+
// Issue #641(beta 第二阶段):分诊账本的档位。#616 点名的父子绑定不继承——
|
|
382
|
+
// enforce 与 freeze 同开时 freeze 让位(本相位降级 shadow + warn):freeze 照常
|
|
383
|
+
// 停车(shadow 语义 = 只记账零处置),绝不出现两套处置语义同时生效。宁可少处置,
|
|
384
|
+
// 不在打架的配置下跑。
|
|
385
|
+
let ledgerMode = config.conflictLedgerMode ?? "off";
|
|
386
|
+
if (ledgerMode === "enforce" && config.conflictFreezeEnabled === true) {
|
|
387
|
+
logger?.warn?.("dsh-mneme sleep: conflictLedgerMode=enforce coexists with conflictFreezeEnabled — degrading to shadow; disable one of them");
|
|
388
|
+
ledgerMode = "shadow";
|
|
389
|
+
}
|
|
390
|
+
// alreadyJudged 预筛只为省 LLM:freeze 与 enforce 都不走 LLM,筛不筛不影响成本,
|
|
391
|
+
// 不筛还能让 T2/T3/T4 的每轮观察持续刷新账本(tier 可能随内容更新而变)。
|
|
392
|
+
const selectionPairs = config.conflictFreezeEnabled === true || ledgerMode === "enforce"
|
|
380
393
|
? fresh
|
|
381
394
|
: fresh.filter((p) => isCrossScopePair(p) || !alreadyJudged(p));
|
|
382
395
|
const ledgerSkipped = fresh.length - selectionPairs.length;
|
|
@@ -390,6 +403,115 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
390
403
|
selected.push(p);
|
|
391
404
|
}
|
|
392
405
|
|
|
406
|
+
// Issue #641(beta 第一阶段,shadow 档):分诊账本。对 selected 全量做确定性
|
|
407
|
+
// 分诊(dup / cross-scope-dup / evolution / conflict,判据唯一实现见 triage.js)
|
|
408
|
+
// 并落 conflict_ledger。shadow = 只记账零处置:下面 freeze/LLM 路径一个字节都
|
|
409
|
+
// 不变——shadow 的存在意义就是让「分诊分布是否复现真实库 46/20/5/27」在真实
|
|
410
|
+
// 运行里可测,作为切 enforce 前的预注册证据。记账失败只 warn 计数,绝不反噬
|
|
411
|
+
// 相位(与 conflict_reviewed 同一条 best-effort 纪律)。
|
|
412
|
+
// 记账时点在 used 去重之后:被去重压掉的那半边对(同一条记忆配给了更高相似的
|
|
413
|
+
// 对手)本来就不是这一轮的候选,观察它们只会让分母虚高。
|
|
414
|
+
const ledgerTriage = ledgerMode !== "off"
|
|
415
|
+
? await recordLedgerTriage(service, runId, selected, {
|
|
416
|
+
dupMinSimilarity: config.conflictLedgerDupMinSimilarity,
|
|
417
|
+
logger,
|
|
418
|
+
// 分块记账的取消检查:用户活动打断时停止记账,余下的对下一轮重见
|
|
419
|
+
signal
|
|
420
|
+
})
|
|
421
|
+
: null;
|
|
422
|
+
|
|
423
|
+
// Issue #641(beta 第二阶段,enforce 档):确定性处置接线。T1(dup)走
|
|
424
|
+
// supersedeInfoFirstPair(与冻结分支自动档同一条契约,decisions.js 共享实现);
|
|
425
|
+
// T1 走信息量优先 supersede、T3 走取代链(superseded_by 指向,不归档)、
|
|
426
|
+
// T2/T4 记账即处置——不停车、不送 LLM。人审队列与 LLM 裁决在本档下都不再
|
|
427
|
+
// 消费冲突候选,conflict_pending 恒空。每轮上限沿用
|
|
428
|
+
// sleepConflictMaxPairsPerRun:处置虽不走 LLM,归档/落链仍是写动作,写预算
|
|
429
|
+
// 与 freeze/auto 两档同口径。T4 的检索时消解(fuseRecall 矛盾感知 + 注入
|
|
430
|
+
// 标注)是后续 PR,本档先把人审摘出去。
|
|
431
|
+
if (ledgerMode === "enforce") {
|
|
432
|
+
const perRunCap = config.sleepConflictMaxPairsPerRun ?? 60;
|
|
433
|
+
const pool = perRunCap > 0 ? selected.slice(0, perRunCap) : selected;
|
|
434
|
+
let t1Resolved = 0;
|
|
435
|
+
let t1Failed = 0;
|
|
436
|
+
let t3Chained = 0;
|
|
437
|
+
const t1VectorFixups = [];
|
|
438
|
+
for (const p of pool) {
|
|
439
|
+
const verdict = triagePair(p.a, p.b, p.similarity, { dupMinSimilarity: config.conflictLedgerDupMinSimilarity });
|
|
440
|
+
if (verdict.tier === "evolution") {
|
|
441
|
+
// T3 取代链(beta 第三阶段):旧行 superseded_by → 新行,检索降权,
|
|
442
|
+
// **不归档**——演进不是输赢,旧值仍是「曾经为真」的可追溯历史(与
|
|
443
|
+
// supersede 动作的归档语义不同)。新者胜对演进成立:间隔 ≥7 天的
|
|
444
|
+
// 同主题对,新记载才反映现状(服务器时钟,内容声称的时间不做键)。
|
|
445
|
+
const newer = (p.a.created_at ?? "") >= (p.b.created_at ?? "") ? p.a : p.b;
|
|
446
|
+
const older = newer === p.a ? p.b : p.a;
|
|
447
|
+
try {
|
|
448
|
+
if (service.supersedeChain(older.id, newer.id)) {
|
|
449
|
+
t3Chained += 1;
|
|
450
|
+
// 先落观察行再推进状态(#650 评审):enforce 档下 recordLedgerTriage 只会
|
|
451
|
+
// 把这一对写成 open,UPDATE 于此才有行可命中;顺序反了就是静默落空。
|
|
452
|
+
service.saveConflictLedgerEntry({ run_id: runId, memory_a: p.a.id, memory_b: p.b.id, tier: verdict.tier, similarity: p.similarity, signals: verdict.signals });
|
|
453
|
+
service.setConflictLedgerStatus(p.a.id, p.b.id, "superseded");
|
|
454
|
+
}
|
|
455
|
+
} catch (error) {
|
|
456
|
+
logger?.warn?.(`dsh-mneme sleep: ledger T3 chain failed for ${p.a.id}/${p.b.id}: ${String(error)}`);
|
|
457
|
+
}
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (verdict.tier !== "dup") continue;
|
|
461
|
+
let res = null;
|
|
462
|
+
try {
|
|
463
|
+
res = supersedeInfoFirstPair(p, service, logger, config);
|
|
464
|
+
} catch (error) {
|
|
465
|
+
logger?.warn?.(`dsh-mneme sleep: ledger T1 supersede failed for ${p.a.id}/${p.b.id}: ${String(error)}`);
|
|
466
|
+
}
|
|
467
|
+
if (res?.applied) {
|
|
468
|
+
t1Resolved += 1;
|
|
469
|
+
// winner/loser 用 helper 判定的真实方向(信息量优先可能翻转 p.a/p.b)
|
|
470
|
+
t1VectorFixups.push({ action: "supersede", winner: res.winnerId, loser: res.loserId });
|
|
471
|
+
try {
|
|
472
|
+
// 账本状态推进 + 机器台账照记(与自动档同款 outcome 口径)——自动处置
|
|
473
|
+
// 也要能事后回答「这一对什么时候、被谁判的」。先落观察行(#650 评审):
|
|
474
|
+
// 这一对未必已在账本里,直接 UPDATE 会命中 0 行而静默失记。
|
|
475
|
+
service.saveConflictLedgerEntry({ run_id: runId, memory_a: p.a.id, memory_b: p.b.id, tier: verdict.tier, similarity: p.similarity, signals: verdict.signals });
|
|
476
|
+
service.setConflictLedgerStatus(p.a.id, p.b.id, "superseded");
|
|
477
|
+
service.saveConflictReviewed?.({ run_id: runId, memory_a: p.a.id, memory_b: p.b.id, outcome: "supersede", judged_at: new Date().toISOString() });
|
|
478
|
+
} catch (error) {
|
|
479
|
+
logger?.warn?.(`dsh-mneme sleep: ledger bookkeeping after T1 supersede failed for ${p.a.id}/${p.b.id}: ${String(error)}`);
|
|
480
|
+
}
|
|
481
|
+
} else {
|
|
482
|
+
t1Failed += 1;
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
// 败者向量清理:T1 的 supersede 不在下面 LLM 通路的 maintainIndexAfterDream
|
|
486
|
+
// 清单里(enforce 已早退),必须在早退前自己清——已归档行留向量等于检索
|
|
487
|
+
// 层「看得见摸不着」。
|
|
488
|
+
if (t1VectorFixups.length > 0 && semantic?.embedder && semantic?.vectorIndex) {
|
|
489
|
+
try {
|
|
490
|
+
await maintainIndexAfterDream(t1VectorFixups, service, semantic);
|
|
491
|
+
} catch (error) {
|
|
492
|
+
logger?.warn?.(`dsh-mneme sleep: ledger T1 index maintenance failed: ${String(error)}`);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
if (selected.length > pool.length) {
|
|
496
|
+
logger?.warn?.(`dsh-mneme sleep: conflict ledger enforce capped at ${pool.length}/${selected.length} pairs this run`);
|
|
497
|
+
}
|
|
498
|
+
return {
|
|
499
|
+
status: t1Failed > 0 ? "degraded" : t1Resolved > 0 || t3Chained > 0 ? "ok" : "noop",
|
|
500
|
+
clean: t1Failed === 0,
|
|
501
|
+
frozen: 0,
|
|
502
|
+
scopeCandidates: 0,
|
|
503
|
+
pairs: selected.length,
|
|
504
|
+
batches: 0,
|
|
505
|
+
ledgerSkipped,
|
|
506
|
+
// applied 是整轮回执的唯一写库口径(runSleep 的 totalApplied 只认这个键)。
|
|
507
|
+
// 缺了它,本档真实发生的 T1 归档与 T3 落链在 receipt / dream_runs.applied
|
|
508
|
+
// 里全是 0 ——「本相位说改了、整轮说没改」(#650 评审,#619 同族)。
|
|
509
|
+
applied: t1Resolved + t3Chained,
|
|
510
|
+
enforced: { t1Resolved, t1Failed, t3Chained },
|
|
511
|
+
...(ledgerTriage ? { ledgerTriage } : {})
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
393
515
|
// v0.8.1(issue #170 第 2 步):跨 scope 相似对分流。同一内容落在两个归属下
|
|
394
516
|
// (任一维标注键不等即算,NULL=未标注=全局也参与比较——「全局副本 + 专属副本
|
|
395
517
|
// 并存」正是收窄/提升裁决的典型候选)不能自动裁决:归档败者可能销毁该内容在
|
|
@@ -583,7 +705,10 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
583
705
|
batches: 0,
|
|
584
706
|
ledgerSkipped,
|
|
585
707
|
perRunSkipped,
|
|
586
|
-
pendingSkipped
|
|
708
|
+
pendingSkipped,
|
|
709
|
+
// #641 shadow:分诊分布随相位结果透出(dream phases 与审计行都能看到),
|
|
710
|
+
// 面板消费在 beta 第二批接。
|
|
711
|
+
...(ledgerTriage ? { ledgerTriage } : {})
|
|
587
712
|
};
|
|
588
713
|
}
|
|
589
714
|
|
|
@@ -601,13 +726,14 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
601
726
|
scopeCandidates: scopeParked,
|
|
602
727
|
pairs: selected.length,
|
|
603
728
|
batches: 0,
|
|
604
|
-
ledgerSkipped
|
|
729
|
+
ledgerSkipped,
|
|
730
|
+
...(ledgerTriage ? { ledgerTriage } : {})
|
|
605
731
|
};
|
|
606
732
|
}
|
|
607
733
|
|
|
608
734
|
// LLM adjudication(仅同 scope 对)。
|
|
609
735
|
const route = resolveSleepRoute(ctx, config, logger);
|
|
610
|
-
if (!route) return { status: "skipped", reason: "no llm route", scopeCandidates: scopeParked, pairs: selected.length, batches: 0, ledgerSkipped };
|
|
736
|
+
if (!route) return { status: "skipped", reason: "no llm route", scopeCandidates: scopeParked, pairs: selected.length, batches: 0, ledgerSkipped, ...(ledgerTriage ? { ledgerTriage } : {}) };
|
|
611
737
|
// 每轮总上限:分批解决的是单请求撞墙,不是"一个 449 对的池子该在一轮里跑完"。
|
|
612
738
|
// 没进这一轮的对留在下一轮(它们既没被丢弃,也不在台账里)。
|
|
613
739
|
const perRunCap = config.sleepConflictMaxPairsPerRun ?? 60;
|
|
@@ -907,7 +1033,10 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
907
1033
|
conflicts,
|
|
908
1034
|
// Issue #126:#104 同向——被跳过的逐对明细要进审计行,否则事后无法判断
|
|
909
1035
|
// 是哪一对、因何被跳过(此前只进 logger.warn,splice 后就地消失)。
|
|
910
|
-
skipped
|
|
1036
|
+
skipped,
|
|
1037
|
+
// #641 shadow:分诊分布随相位结果透出(四个出口都要带——freeze/空池/无路由/
|
|
1038
|
+
// LLM 全路径,缺一个出口 shadow 期就有部分轮次查不到分布)。
|
|
1039
|
+
...(ledgerTriage ? { ledgerTriage } : {})
|
|
911
1040
|
};
|
|
912
1041
|
}
|
|
913
1042
|
|
|
@@ -1169,7 +1298,18 @@ async function phaseAffective(ctx, service, config, logger, runId, signal = null
|
|
|
1169
1298
|
}, reportUsage, (reason) => { streamFailure = describeStreamFailure(reason); })
|
|
1170
1299
|
);
|
|
1171
1300
|
const text = await withMaxTokensFallback(ctx, config.sleepMaxTokens ?? 2048, runAffective, () => streamFailure);
|
|
1172
|
-
if (text === undefined)
|
|
1301
|
+
if (text === undefined) {
|
|
1302
|
+
// #619 可见化 A:失败原因不能只写成 "llm failed"。conflict 相位
|
|
1303
|
+
// (src/dream/sleep.js 的 adjudicateBatch 调用处)早就把 stream 层的原文写进
|
|
1304
|
+
// warn 并挂在返回值的 `failure` 上,理由写在它旁边的注释里:只写 "llm failed"
|
|
1305
|
+
// 的话,运维分不清是上下文超限、模型拒收还是用户活动打断。affective 相位漏了
|
|
1306
|
+
// 这一步,实测 2026-11-10T16:09 那一轮审计行只有 {"status":"failed",
|
|
1307
|
+
// "error":"llm failed"},502 原文只能去 llm_audit_logs 里捞。
|
|
1308
|
+
// 相位自身不重试 5xx(withMaxTokensFallback 只处理 max_tokens 被拒、
|
|
1309
|
+
// withEffortFallback 只处理 effort 被拒),所以这里记的就是一次性失败的真因。
|
|
1310
|
+
if (streamFailure) logger?.warn?.(`dsh-mneme sleep: affective stream aborted or errored (${streamFailure})`);
|
|
1311
|
+
return { status: "failed", error: "llm failed", failure: streamFailure || undefined };
|
|
1312
|
+
}
|
|
1173
1313
|
const items = parseJsonArray(text) ?? [];
|
|
1174
1314
|
if (items.length === 0) return { status: "skipped", reason: "no attributions found" };
|
|
1175
1315
|
const relations = [];
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
// 冲突分诊(issue #641 beta 第一阶段,shadow 档):把 findPotentialConflicts 送进
|
|
2
|
+
// 冲突相位的每一对,先过确定性分诊,落进 conflict_ledger 账本。
|
|
3
|
+
//
|
|
4
|
+
// 为什么分诊先行:真实库 788 对人审队列的全量分类(只读挖掘,口径见 #641 正文)
|
|
5
|
+
// ——46% 近重复 + 20% 跨作用域重复 + 5% 时序演进,真矛盾只有 ~27%。前 71% 是该被
|
|
6
|
+
// 机械处置(merge / 去重 / 取代链)的工作量,现行窄 prompt(conflict/keep 二选一)
|
|
7
|
+
// 把它们全逼成了「输赢」,再由人来逐对裁决(≈30 条/天,且 92% 的对从未两侧同场)。
|
|
8
|
+
// 分诊把「谁该机械处理、谁才是真矛盾」从 LLM 手里拿回来:E13b 实测 winner-take-all
|
|
9
|
+
// 裁决 P(毒胜出)=86.8%,LLM 在这条链上只允许降级为分类器。
|
|
10
|
+
//
|
|
11
|
+
// 四档判据(全部确定性,本文件是唯一实现;enforce 阶段的处置接线另有 PR,本文件
|
|
12
|
+
// 在 shadow 档只产分类与信号,不做任何记忆行处置):
|
|
13
|
+
// cross-scope-dup 任一 scope 维键不同——同一内容落两个归属(#170 scopeReason 口径,
|
|
14
|
+
// 真实库占 20%)
|
|
15
|
+
// dup 同 scope + sim ≥ dupMin + created_at 差 < 24h——同日双录
|
|
16
|
+
// (真实库 76% 的对两侧差 <1 天;#616 实测 55-57% 的新条反而更短,
|
|
17
|
+
// 新鲜度对这类对无区分力,后续处置按信息量裁决)
|
|
18
|
+
// evolution 同 scope + created_at 差 ≥ 7 天——时序演进展期(T3 superseded_by
|
|
19
|
+
// 取代链的候选池,enforce 阶段接线;entity_attrs 失效信号并入也在
|
|
20
|
+
// 那一步,见 #585)
|
|
21
|
+
// conflict 其余——真矛盾残余(真实库 ~27%;账本标记,消解推迟到检索时/
|
|
22
|
+
// 会话纠正/巩固重蒸馏三时刻)
|
|
23
|
+
//
|
|
24
|
+
// 「未标注 scope」不算跨作用域:scopeKeyOf 把 NULL 归一成 null,两条都未标注时
|
|
25
|
+
// null !== null 为 false、判同档——#616 指出这个判据用于**自动归档**是洞(归属未知
|
|
26
|
+
// ≠归属相同),但在 shadow 分诊里零处置、无害;signals.annotated=false 显式落账,
|
|
27
|
+
// 让分析时能把「未标注」切出来,而不是让它静默混进同 scope 桶。
|
|
28
|
+
import { scopeKeyOf } from "../scope.js";
|
|
29
|
+
|
|
30
|
+
const DAY_MS = 86_400_000;
|
|
31
|
+
|
|
32
|
+
// T1(dup)与 T3(evolution)的时间界。24h 来自真实库分布(76% 的对 <1 天,且
|
|
33
|
+
// 1-7 天区间的「疑似同日双录」残留在 1-7 天档里很难与快速演进区分,宁保守归
|
|
34
|
+
// conflict——分诊错档在 shadow 期只是统计噪音,enforce 后错档会错杀);7 天是
|
|
35
|
+
// 演进展期的保守下限,真实库 7 天以上的对仅 0.5%(4/788)。
|
|
36
|
+
export const DUP_MAX_AGE_MS = DAY_MS;
|
|
37
|
+
export const EVOLUTION_MIN_AGE_MS = 7 * DAY_MS;
|
|
38
|
+
|
|
39
|
+
export const LEDGER_TIERS = ["dup", "cross-scope-dup", "evolution", "conflict"];
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Classify one candidate pair into a ledger tier. Pure function — no I/O, no
|
|
43
|
+
* config dependency beyond the tunables passed in, so the same verdict is
|
|
44
|
+
* reproducible from the recorded signals alone.
|
|
45
|
+
*
|
|
46
|
+
* @param {object} a candidate side A (needs agent_scope, workspace_scope, created_at)
|
|
47
|
+
* @param {object} b candidate side B
|
|
48
|
+
* @param {number} similarity detector cosine similarity (pair already passed the
|
|
49
|
+
* detection threshold, so ≥ ~0.75-0.92 depending on strictness)
|
|
50
|
+
* @param {{dupMinSimilarity?: number, now?: number}} opts dupMin 默认 0.90(#616
|
|
51
|
+
* 重测:0.95 在现库覆盖为 0,真实库 reason 相似度回声集中在 0.85-0.93,
|
|
52
|
+
* 0.90 是把回声带接进 dup 档的落点);now 仅测试注入
|
|
53
|
+
* @returns {{tier: string, signals: object}}
|
|
54
|
+
*/
|
|
55
|
+
export function triagePair(a, b, similarity, opts = {}) {
|
|
56
|
+
const dupMin = typeof opts.dupMinSimilarity === "number" ? opts.dupMinSimilarity : 0.9;
|
|
57
|
+
const now = typeof opts.now === "number" ? opts.now : Date.now();
|
|
58
|
+
|
|
59
|
+
const crossScope =
|
|
60
|
+
scopeKeyOf(a.agent_scope) !== scopeKeyOf(b.agent_scope) ||
|
|
61
|
+
scopeKeyOf(a.workspace_scope) !== scopeKeyOf(b.workspace_scope);
|
|
62
|
+
const annotated = (a.agent_scope != null || a.workspace_scope != null) &&
|
|
63
|
+
(b.agent_scope != null || b.workspace_scope != null);
|
|
64
|
+
|
|
65
|
+
// created_at 缺失不下结论:宁归 conflict(账本里最保守的一档),也不编造时间信号。
|
|
66
|
+
const createdA = Date.parse(a.created_at ?? "");
|
|
67
|
+
const createdB = Date.parse(b.created_at ?? "");
|
|
68
|
+
const gapKnown = Number.isFinite(createdA) && Number.isFinite(createdB);
|
|
69
|
+
const gapDays = gapKnown ? Math.abs(createdA - createdB) / DAY_MS : null;
|
|
70
|
+
|
|
71
|
+
let tier;
|
|
72
|
+
if (crossScope) {
|
|
73
|
+
tier = "cross-scope-dup";
|
|
74
|
+
} else if (gapKnown && typeof similarity === "number" && similarity >= dupMin && gapDays < DUP_MAX_AGE_MS / DAY_MS) {
|
|
75
|
+
tier = "dup";
|
|
76
|
+
} else if (gapKnown && gapDays * DAY_MS >= EVOLUTION_MIN_AGE_MS) {
|
|
77
|
+
tier = "evolution";
|
|
78
|
+
} else {
|
|
79
|
+
tier = "conflict";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
tier,
|
|
84
|
+
signals: {
|
|
85
|
+
crossScope,
|
|
86
|
+
annotated,
|
|
87
|
+
gapDays: gapDays == null ? null : Math.round(gapDays * 1000) / 1000,
|
|
88
|
+
dupMin,
|
|
89
|
+
similarity: typeof similarity === "number" ? similarity : null
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Triage + record every selected pair into the conflict_ledger. Bookkeeping
|
|
96
|
+
* only: a store failure on one pair must never fail the phase (same
|
|
97
|
+
* best-effort discipline as saveConflictReviewed / saveDifferentiatedPair —
|
|
98
|
+
* 记账本身绝不能反过来把巩固打断)。
|
|
99
|
+
*
|
|
100
|
+
* 分块 + 让出(#643 review):候选池大时同步写完全部对会连续占用事件循环、
|
|
101
|
+
* 延迟用户活动与取消信号的处理——每块之间 setImmediate 让出一次,并在每块
|
|
102
|
+
* 开始前检查 signal.aborted。取消后停止记账,余下的对下一轮自然重见;**不用
|
|
103
|
+
* 固定上限裁剪观察样本**(shadow 的分母必须完整,截断只来自取消,且在
|
|
104
|
+
* tally.truncatedAt 可见——静默截断会让分布失真,比没有数据更糟)。
|
|
105
|
+
*
|
|
106
|
+
* @returns Promise<tally> {dup, cross-scope-dup, evolution, conflict, recorded,
|
|
107
|
+
* failed, truncatedAt} — truncatedAt 非 null 表示取消点(已处理的对数)
|
|
108
|
+
*/
|
|
109
|
+
export async function recordLedgerTriage(service, runId, pairs, opts = {}) {
|
|
110
|
+
const tally = { dup: 0, "cross-scope-dup": 0, evolution: 0, conflict: 0, recorded: 0, failed: 0, truncatedAt: null };
|
|
111
|
+
const CHUNK = 50;
|
|
112
|
+
for (let start = 0; start < pairs.length; start += CHUNK) {
|
|
113
|
+
// 块间让出事件循环(首块前不用——pairs 为空或单块时零额外宏任务)
|
|
114
|
+
if (start > 0) await new Promise((resolve) => setImmediate(resolve));
|
|
115
|
+
if (opts.signal?.aborted) {
|
|
116
|
+
tally.truncatedAt = start;
|
|
117
|
+
break;
|
|
118
|
+
}
|
|
119
|
+
for (const p of pairs.slice(start, start + CHUNK)) {
|
|
120
|
+
try {
|
|
121
|
+
const verdict = triagePair(p.a, p.b, p.similarity, opts);
|
|
122
|
+
service.saveConflictLedgerEntry({
|
|
123
|
+
run_id: runId,
|
|
124
|
+
memory_a: p.a.id,
|
|
125
|
+
memory_b: p.b.id,
|
|
126
|
+
tier: verdict.tier,
|
|
127
|
+
similarity: p.similarity,
|
|
128
|
+
signals: verdict.signals
|
|
129
|
+
});
|
|
130
|
+
tally[verdict.tier] += 1;
|
|
131
|
+
tally.recorded += 1;
|
|
132
|
+
} catch (error) {
|
|
133
|
+
tally.failed += 1;
|
|
134
|
+
opts.logger?.warn?.(`dsh-mneme sleep: failed to record ledger triage for ${p.a?.id}/${p.b?.id}: ${String(error)}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return tally;
|
|
139
|
+
}
|
package/src/dream.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import { validateDecisions, applyDecisions } from "./dream/decisions.js";
|
|
1
|
+
import { validateDecisions, applyDecisions, supersedeInfoFirstPair } from "./dream/decisions.js";
|
|
2
2
|
import { clusterMemories, findPotentialConflicts, cosineSimilarity } from "./dream/clustering.js";
|
|
3
|
+
import { triagePair } from "./dream/triage.js";
|
|
3
4
|
import { clusterByTag, intersectEvidence } from "./dream/narratives.js";
|
|
4
5
|
import { scopeKeyOf } from "./scope.js";
|
|
5
6
|
// Issue #239(第 4 项)镜像到巩固:错峰时段解析与「最近的高峰结束时刻」从独立的
|
|
@@ -967,7 +968,7 @@ export async function maintainIndexAfterDream(decisions, service, semantic) {
|
|
|
967
968
|
// 用户配得比这更大的 dreamMinIntervalMinutes 基数压小(见 effectiveMinIntervalMs)。
|
|
968
969
|
const FAILURE_BACKOFF_CAP_MS = 30 * 60 * 1000;
|
|
969
970
|
|
|
970
|
-
export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChars = 5000, delayMs = 2000, minIntervalMs = 0, failureBackoff = false, logger, semantic = null, lastRunAtSeed = 0, baselineSeed = null, peakHours = "", peakMaxDeferMinutes = 120, auditPeakSkip = null, now = () => Date.now(), setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout }) {
|
|
971
|
+
export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChars = 5000, delayMs = 2000, minIntervalMs = 0, failureBackoff = false, logger, semantic = null, lastRunAtSeed = 0, baselineSeed = null, peakHours = "", peakMaxDeferMinutes = 120, auditPeakSkip = null, auditBaselineClamp = null, now = () => Date.now(), setTimeoutFn = setTimeout, clearTimeoutFn = clearTimeout }) {
|
|
971
972
|
let pendingTimer = null;
|
|
972
973
|
// Issue #239(第 4 项)镜像到巩固:高峰顺延定时器。与 pendingTimer 分开——两者
|
|
973
974
|
// 语义不同(一个是「马上要跑」,一个是「等出高峰再跑」),合成一个变量会让
|
|
@@ -1006,6 +1007,27 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1006
1007
|
|
|
1007
1008
|
function shouldTrigger(service) {
|
|
1008
1009
|
const { count, chars } = activeStoreSize(service);
|
|
1010
|
+
// Issue #634(基线夹回):基线只在成功轮前进(见下方 startRun 与 #89 注释),
|
|
1011
|
+
// 而归档/删除/合并把活跃量压下去——活跃量一旦跌破基线,overBase 恒为假,
|
|
1012
|
+
// 从此再也不会巩固,且全程静默(#634 实测停摆 13 天 / 22 小时零轮)。修法:
|
|
1013
|
+
// 判定前把基线夹回当前活跃量(每轴取 min),阈值从新的低点重新起算,归掉的
|
|
1014
|
+
// 量不必重新长回来。只动闭包内存、不落库:下一次成功轮本来就会把当时的规模
|
|
1015
|
+
// 写成新基线(startRun 的 result.baseline / 现场计算),重启后种子若仍是旧峰值,
|
|
1016
|
+
// 第一次判定在这里同样被夹回——所以不需要新的持久化形状。收缩的来源不止归档
|
|
1017
|
+
// (删除、合并、暂停作用域、外来行占比变化同效果),且归档路径不触发
|
|
1018
|
+
// notifyWrite(service.notifyWrite:状态开关不算内容写入),逐写路径维护基线
|
|
1019
|
+
// 碰的面大、漏面也大——只在判定口一处收敛。夹回是罕见事件(每次「收缩→再触发」
|
|
1020
|
+
// 周期至多一次:夹回后基线 == 活跃量,直到下一轮成功把基线抬高),审计照
|
|
1021
|
+
// auditPeakSkip 的 fail-safe 口径走,记的是「这次收缩把阈值起点拉下来了」。
|
|
1022
|
+
if (count < baseline.count || chars < baseline.chars) {
|
|
1023
|
+
const previous = baseline;
|
|
1024
|
+
baseline = { count: Math.min(baseline.count, count), chars: Math.min(baseline.chars, chars) };
|
|
1025
|
+
try {
|
|
1026
|
+
auditBaselineClamp?.({ previous, count: baseline.count, chars: baseline.chars });
|
|
1027
|
+
} catch (error) {
|
|
1028
|
+
logger?.warn?.(`dsh-mneme dream: baseline-clamp audit failed: ${String(error)}`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1009
1031
|
const overBase = count >= baseline.count + thresholdCount || chars >= baseline.chars + thresholdChars;
|
|
1010
1032
|
const overAbs = count >= thresholdCount || chars >= thresholdChars;
|
|
1011
1033
|
return { trigger: overAbs && overBase, count, chars };
|
|
@@ -1487,6 +1509,11 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1487
1509
|
// bounds the review queue; overflow is skipped with a warning.
|
|
1488
1510
|
let frozenCount = 0;
|
|
1489
1511
|
const frozenIds = [];
|
|
1512
|
+
// Issue #641(enforce 档):T1 supersede 的实质产出计数与向量清理清单。声明在
|
|
1513
|
+
// freeze/enforce 两个分支外层——noChange 判定与 maintainIndexAfterDream 都在
|
|
1514
|
+
// 它们之后、函数作用域里消费。
|
|
1515
|
+
let ledgerApplied = 0;
|
|
1516
|
+
let ledgerVectorFixups = [];
|
|
1490
1517
|
// v0.8.1(issue #170 复核项 3):跨 scope 的 conflict 决策两种模式都一律
|
|
1491
1518
|
// 停车人工裁决、绝不自动归档——否则普通 dream 的 LLM 能归档别的 scope 的
|
|
1492
1519
|
// 唯一副本,sleep 路径(scope-candidate-queue)的保护在这里缺位。同 scope
|
|
@@ -1499,9 +1526,90 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1499
1526
|
|| scopeKeyOf(a.workspace_scope) !== scopeKeyOf(b.workspace_scope)
|
|
1500
1527
|
);
|
|
1501
1528
|
};
|
|
1502
|
-
|
|
1529
|
+
// Issue #641(beta 第二阶段,enforce 档):dream 主链路的 conflict 决策改为
|
|
1530
|
+
// 确定性处置。LLM 判出的 conflict 对先经 triagePair 再分诊:判为 dup 的走
|
|
1531
|
+
// supersedeInfoFirstPair(与 sleep 自动档同一条契约);其余(cross-scope-dup /
|
|
1532
|
+
// evolution / conflict)只落 conflict_ledger 账本(tier + 信号),不停车、
|
|
1533
|
+
// 不归档。#616 的父子绑定不继承:enforce 与 freeze 同开时 freeze 让位——
|
|
1534
|
+
// 本段降级 shadow(conflict 照旧停车),绝不两套处置语义同时生效。
|
|
1535
|
+
const ledgerEnforce = (config.conflictLedgerMode ?? "off") === "enforce" && !freezeEnabled;
|
|
1536
|
+
if ((config.conflictLedgerMode ?? "off") === "enforce" && freezeEnabled) {
|
|
1537
|
+
logger?.warn?.("dsh-mneme dream: conflictLedgerMode=enforce coexists with conflictFreezeEnabled — degrading to freeze; disable one of them");
|
|
1538
|
+
}
|
|
1539
|
+
if (ledgerEnforce) {
|
|
1540
|
+
let t1Resolved = 0;
|
|
1541
|
+
let t3Chained = 0;
|
|
1542
|
+
let marked = 0;
|
|
1543
|
+
for (const d of decisions.filter((x) => x.action === "conflict")) {
|
|
1544
|
+
const a = snapshot.get(d.winner);
|
|
1545
|
+
const b = snapshot.get(d.loser);
|
|
1546
|
+
if (!a || !b) continue;
|
|
1547
|
+
try {
|
|
1548
|
+
// 判据必须与 sleep 侧同源(#650 评审):sleep enforce 传的是
|
|
1549
|
+
// config.conflictLedgerDupMinSimilarity,这里不传就会退回 triage.js 的
|
|
1550
|
+
// 死默认 0.9 —— 用户把阈值调高/调低后,同一对在两条路径上可能分到不同
|
|
1551
|
+
// tier(一侧判 dup 去归档,另一侧判 conflict 只记账)。
|
|
1552
|
+
const verdict = triagePair(a, b, pairSimilarity.get(similarityKey(d.winner, d.loser)), {
|
|
1553
|
+
dupMinSimilarity: config.conflictLedgerDupMinSimilarity
|
|
1554
|
+
});
|
|
1555
|
+
if (verdict.tier === "dup") {
|
|
1556
|
+
const res = supersedeInfoFirstPair({ a, b }, service, logger, config);
|
|
1557
|
+
if (res.applied) {
|
|
1558
|
+
t1Resolved += 1;
|
|
1559
|
+
ledgerApplied += 1;
|
|
1560
|
+
// 败者向量清理走下面的 maintainIndexAfterDream:T1 的 supersede 是
|
|
1561
|
+
// 本 helper 独立落地的,不在 applyList 里,必须显式补进维护清单
|
|
1562
|
+
// (否则索引里留一行已归档记忆的向量,「清得掉才检索得到」失衡)。
|
|
1563
|
+
ledgerVectorFixups.push({ action: "supersede", winner: res.winnerId, loser: res.loserId });
|
|
1564
|
+
// 先落观察行再推进状态(#650 评审):dream 路径不像 sleep 那样先跑
|
|
1565
|
+
// recordLedgerTriage,账本里可能根本没有这一对;此处若直接 UPDATE,
|
|
1566
|
+
// 命中 0 行、changes=0 被静默丢弃,于是「处置成功」与「账本有记录」
|
|
1567
|
+
// 长期背离。upsert 一次拿到确定的行,状态推进才有落点。
|
|
1568
|
+
service.saveConflictLedgerEntry({ run_id: runId, memory_a: a.id, memory_b: b.id, tier: verdict.tier, similarity: verdict.signals.similarity, signals: verdict.signals });
|
|
1569
|
+
service.setConflictLedgerStatus(a.id, b.id, "superseded");
|
|
1570
|
+
try {
|
|
1571
|
+
service.saveConflictReviewed?.({ run_id: runId, memory_a: a.id, memory_b: b.id, outcome: "supersede", judged_at: new Date().toISOString() });
|
|
1572
|
+
} catch (error) {
|
|
1573
|
+
logger?.warn?.(`dsh-mneme dream: conflict_reviewed bookkeeping failed for ${a.id}/${b.id}: ${String(error)}`);
|
|
1574
|
+
}
|
|
1575
|
+
} else {
|
|
1576
|
+
// CAS 没过(两侧在快照后被改过):不冒险自动处置,落账本留观察。
|
|
1577
|
+
service.saveConflictLedgerEntry({ run_id: runId, memory_a: a.id, memory_b: b.id, tier: verdict.tier, similarity: verdict.signals.similarity, signals: verdict.signals });
|
|
1578
|
+
}
|
|
1579
|
+
} else if (verdict.tier === "evolution") {
|
|
1580
|
+
// T3 取代链(与 sleep enforce 同款):旧行 superseded_by → 新行,
|
|
1581
|
+
// 不归档、不进 applyList。快照行可能缺 created_at(老决策),缺了
|
|
1582
|
+
// 就退回纯记账——宁可少处置,不编造时间方向。
|
|
1583
|
+
const newer = (a.created_at ?? "") >= (b.created_at ?? "") ? a : b;
|
|
1584
|
+
const older = newer === a ? b : a;
|
|
1585
|
+
try {
|
|
1586
|
+
if (service.supersedeChain(older.id, newer.id)) {
|
|
1587
|
+
t3Chained += 1;
|
|
1588
|
+
service.saveConflictLedgerEntry({ run_id: runId, memory_a: a.id, memory_b: b.id, tier: verdict.tier, similarity: verdict.signals.similarity, signals: verdict.signals });
|
|
1589
|
+
service.setConflictLedgerStatus(a.id, b.id, "superseded");
|
|
1590
|
+
} else {
|
|
1591
|
+
service.saveConflictLedgerEntry({ run_id: runId, memory_a: a.id, memory_b: b.id, tier: verdict.tier, similarity: verdict.signals.similarity, signals: verdict.signals });
|
|
1592
|
+
}
|
|
1593
|
+
} catch (error) {
|
|
1594
|
+
logger?.warn?.(`dsh-mneme dream: ledger T3 chain failed for ${a.id}/${b.id}: ${String(error)}`);
|
|
1595
|
+
}
|
|
1596
|
+
} else {
|
|
1597
|
+
service.saveConflictLedgerEntry({ run_id: runId, memory_a: a.id, memory_b: b.id, tier: verdict.tier, similarity: verdict.signals.similarity, signals: verdict.signals });
|
|
1598
|
+
}
|
|
1599
|
+
marked += 1;
|
|
1600
|
+
} catch (error) {
|
|
1601
|
+
logger?.warn?.(`dsh-mneme dream: ledger conflict mark failed for ${d.winner}/${d.loser}: ${String(error)}`);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1604
|
+
if (marked > 0) {
|
|
1605
|
+
logger?.info?.(`[dsh-mneme] dream: ledger enforce marked ${marked} conflict decision(s), ${t1Resolved} superseded, ${t3Chained} chained`);
|
|
1606
|
+
}
|
|
1607
|
+
}
|
|
1608
|
+
const applyList = ledgerEnforce
|
|
1503
1609
|
? decisions.filter((d) => d.action !== "conflict")
|
|
1504
|
-
:
|
|
1610
|
+
: freezeEnabled
|
|
1611
|
+
? decisions.filter((d) => d.action !== "conflict")
|
|
1612
|
+
: decisions.filter((d) => d.action !== "conflict" || !isCrossScopeDecision(d));
|
|
1505
1613
|
if (freezeEnabled) {
|
|
1506
1614
|
const conflictsToFreeze = decisions.filter((d) => d.action === "conflict");
|
|
1507
1615
|
if (conflictsToFreeze.length > 0) {
|
|
@@ -1531,9 +1639,11 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1531
1639
|
logger?.warn?.(`dsh-mneme dream: conflict freeze lookup failed: ${String(error)}`);
|
|
1532
1640
|
}
|
|
1533
1641
|
}
|
|
1534
|
-
} else {
|
|
1642
|
+
} else if (!ledgerEnforce) {
|
|
1535
1643
|
// 非 freeze:跨 scope conflict 停车(绝不进 applyDecisions)。带专属
|
|
1536
1644
|
// reason + 模型给出的裁决理由,队列里读起来是「归属裁决」而非重复项。
|
|
1645
|
+
// ledger enforce 档不进这里:conflict 决策已在上面落账本(tier 含
|
|
1646
|
+
// cross-scope-dup),零停车零归档(#641)。
|
|
1537
1647
|
const crossScope = decisions.filter((d) => d.action === "conflict" && isCrossScopeDecision(d));
|
|
1538
1648
|
for (const d of crossScope) {
|
|
1539
1649
|
try {
|
|
@@ -1606,12 +1716,14 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1606
1716
|
// happened and the scheduler refreshes the baseline on a false positive.
|
|
1607
1717
|
// Frozen conflicts are substantive output (parked for review), so a run
|
|
1608
1718
|
// that only froze conflicts is not a noop.
|
|
1609
|
-
|
|
1719
|
+
// Issue #641:ledger enforce 的 T1 supersede 是独立于 applyList 落地的实质产出,
|
|
1720
|
+
// 计入「本轮有没有干活」,否则有归档产出的轮会被当 noop 刷基线(#634 同族误报)。
|
|
1721
|
+
const noChange = frozenCount === 0 && applied === 0 && ledgerApplied === 0 && committed.every((c) => c.action === "keep");
|
|
1610
1722
|
|
|
1611
1723
|
// Keep the vector index consistent with the post-dream store state.
|
|
1612
1724
|
if (semantic?.embedder && semantic?.vectorIndex) {
|
|
1613
1725
|
try {
|
|
1614
|
-
await maintainIndexAfterDream(applyList, service, semantic);
|
|
1726
|
+
await maintainIndexAfterDream(ledgerVectorFixups.length ? [...applyList, ...ledgerVectorFixups] : applyList, service, semantic);
|
|
1615
1727
|
} catch (error) {
|
|
1616
1728
|
logger?.warn?.(`[dsh-mneme] dream index maintenance failed: ${String(error)}`);
|
|
1617
1729
|
}
|
|
@@ -1746,7 +1858,10 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
1746
1858
|
return finish({
|
|
1747
1859
|
ok: okResult,
|
|
1748
1860
|
status,
|
|
1749
|
-
|
|
1861
|
+
// ledgerApplied 是 ledger enforce 档下 T1 supersede 的独立产出,不在 applyList
|
|
1862
|
+
// 里(#650 评审)。只把它算进 noChange 会让 receipt 与 dream_runs.applied 仍
|
|
1863
|
+
// 报 0:库真被改了,回执说没改——#619 同族假成功。计入 applied 才是唯一口径。
|
|
1864
|
+
applied: applied + ledgerApplied,
|
|
1750
1865
|
decisions: auditDecisions,
|
|
1751
1866
|
outcome,
|
|
1752
1867
|
conflicts,
|