@pencil-agent/nano-pencil 1.11.21 → 1.11.23
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/dist/builtin-extensions.js +9 -0
- package/dist/core/messages.d.ts +1 -0
- package/dist/core/messages.js +4 -0
- package/dist/packages/mem-core/cli.js +84 -0
- package/dist/packages/mem-core/config.d.ts +1 -0
- package/dist/packages/mem-core/config.js +1 -0
- package/dist/packages/mem-core/engine.d.ts +131 -1
- package/dist/packages/mem-core/engine.js +883 -39
- package/dist/packages/mem-core/extension.js +2 -2
- package/dist/packages/mem-core/reconsolidate-v2.js +8 -1
- package/dist/packages/mem-core/types-v2.d.ts +3 -0
- package/dist/packages/mem-core/types.d.ts +6 -0
- package/package.json +2 -2
|
@@ -29,6 +29,8 @@ export class NanoMemEngine {
|
|
|
29
29
|
cfg;
|
|
30
30
|
llmFn;
|
|
31
31
|
embeddingFn;
|
|
32
|
+
static AUTO_V2_LINK_PREFIX = "auto:v2:";
|
|
33
|
+
static AUTO_REVIVE_MAX_ITEMS = 2;
|
|
32
34
|
knowledgePath;
|
|
33
35
|
lessonsPath;
|
|
34
36
|
eventsPath;
|
|
@@ -38,6 +40,13 @@ export class NanoMemEngine {
|
|
|
38
40
|
metaPath;
|
|
39
41
|
episodesDir;
|
|
40
42
|
v2Paths;
|
|
43
|
+
archiveKnowledgePath;
|
|
44
|
+
archiveLessonsPath;
|
|
45
|
+
archiveEventsPath;
|
|
46
|
+
archivePreferencesPath;
|
|
47
|
+
archiveFacetsPath;
|
|
48
|
+
archiveWorkPath;
|
|
49
|
+
archiveV2Paths;
|
|
41
50
|
constructor(overrides, llmFn) {
|
|
42
51
|
this.cfg = getConfig(overrides);
|
|
43
52
|
this.llmFn = llmFn;
|
|
@@ -53,6 +62,14 @@ export class NanoMemEngine {
|
|
|
53
62
|
this.metaPath = join(this.cfg.memoryDir, "meta.json");
|
|
54
63
|
this.episodesDir = join(this.cfg.memoryDir, "episodes");
|
|
55
64
|
this.v2Paths = getV2Paths(this.cfg.memoryDir);
|
|
65
|
+
const archiveDir = join(this.cfg.memoryDir, "_archive");
|
|
66
|
+
this.archiveKnowledgePath = join(archiveDir, "knowledge.json");
|
|
67
|
+
this.archiveLessonsPath = join(archiveDir, "lessons.json");
|
|
68
|
+
this.archiveEventsPath = join(archiveDir, "events.json");
|
|
69
|
+
this.archivePreferencesPath = join(archiveDir, "preferences.json");
|
|
70
|
+
this.archiveFacetsPath = join(archiveDir, "facets.json");
|
|
71
|
+
this.archiveWorkPath = join(archiveDir, "work.json");
|
|
72
|
+
this.archiveV2Paths = getV2Paths(archiveDir);
|
|
56
73
|
}
|
|
57
74
|
setLlmFn(fn) {
|
|
58
75
|
this.llmFn = fn;
|
|
@@ -70,6 +87,7 @@ export class NanoMemEngine {
|
|
|
70
87
|
const events = await loadEntries(this.eventsPath);
|
|
71
88
|
const prefs = await loadEntries(this.preferencesPath);
|
|
72
89
|
const facets = await loadEntries(this.facetsPath);
|
|
90
|
+
const v2Semantic = await loadV2Semantic(this.v2Paths);
|
|
73
91
|
for (const item of items) {
|
|
74
92
|
const target = item.type === "lesson"
|
|
75
93
|
? lessons
|
|
@@ -81,6 +99,7 @@ export class NanoMemEngine {
|
|
|
81
99
|
? facets
|
|
82
100
|
: knowledge;
|
|
83
101
|
applyExtraction(target, item, project, this.cfg);
|
|
102
|
+
this.upsertSemanticFromExtractedItem(v2Semantic, item, project);
|
|
84
103
|
}
|
|
85
104
|
const hl = this.cfg.halfLife;
|
|
86
105
|
const ew = this.cfg.evictionWeights;
|
|
@@ -90,6 +109,7 @@ export class NanoMemEngine {
|
|
|
90
109
|
saveEntries(this.eventsPath, events, this.cfg.maxEntries.events, (e) => utilityEntry(e, hl, ew)),
|
|
91
110
|
saveEntries(this.preferencesPath, prefs, this.cfg.maxEntries.preferences, (e) => utilityEntry(e, hl, ew)),
|
|
92
111
|
saveEntries(this.facetsPath, facets, this.cfg.maxEntries.facets, (e) => utilityEntry(e, hl, ew)),
|
|
112
|
+
saveV2Semantic(this.v2Paths, v2Semantic),
|
|
93
113
|
]);
|
|
94
114
|
return items;
|
|
95
115
|
}
|
|
@@ -156,6 +176,7 @@ export class NanoMemEngine {
|
|
|
156
176
|
return { episodes, facets, semantic, procedural, links };
|
|
157
177
|
}
|
|
158
178
|
async searchV2Memories(query, limit = 10, scope) {
|
|
179
|
+
await this.autoReviveRelevantArchive(query, scope);
|
|
159
180
|
const snapshot = await this.getV2Snapshot();
|
|
160
181
|
const project = scope?.project ?? "";
|
|
161
182
|
const episodes = this.filterAndCleanV2Episodes(snapshot.episodes, project, scope);
|
|
@@ -318,6 +339,7 @@ export class NanoMemEngine {
|
|
|
318
339
|
}
|
|
319
340
|
// ─── Retrieval & Injection (Progressive Recall) ────────────
|
|
320
341
|
async getMemoryInjection(project, contextTags, scope) {
|
|
342
|
+
await this.autoReviveRelevantArchive([project, ...contextTags].join(" ").trim(), scope);
|
|
321
343
|
const [allKnowledge, allLessons, allEvents, allPrefs, allFacets, allEpisodes, allWork, allV2Episodes, allV2EpisodeFacets, allV2Semantic, allProcedural] = await Promise.all([
|
|
322
344
|
loadEntries(this.knowledgePath),
|
|
323
345
|
loadEntries(this.lessonsPath),
|
|
@@ -415,12 +437,32 @@ export class NanoMemEngine {
|
|
|
415
437
|
const cueEpisodeMemories = pickTop(v2Episodes.filter((item) => !activeEpisodeMemories.some((activeItem) => activeItem.id === item.id)), episodeScoreFn, episodeCueLen, Math.max(220, Math.floor(cueChars * 0.12)));
|
|
416
438
|
const cueEpisodeFacets = pickTop(v2EpisodeFacets.filter((item) => !activeEpisodeFacets.some((activeItem) => activeItem.id === item.id)), episodeFacetScoreFn, episodeFacetCueLen, Math.max(220, Math.floor(cueChars * 0.12)));
|
|
417
439
|
const cueSemanticMemories = pickTop(v2Semantic.filter((item) => !activeSemanticMemories.some((activeItem) => activeItem.id === item.id)), semanticMemoryScoreFn, semanticMemoryCueLen, Math.max(220, Math.floor(cueChars * 0.12)));
|
|
440
|
+
const legacyFacetBridgeActive = activeFacets.filter((entry) => entry.facetData?.kind === "pattern" || entry.facetData?.kind === "struggle");
|
|
441
|
+
const legacyFacetBridgeCue = dedupedCueFacets.filter((entry) => entry.facetData?.kind === "pattern" || entry.facetData?.kind === "struggle");
|
|
442
|
+
const v2SignalCount = activeEpisodeMemories.length +
|
|
443
|
+
activeEpisodeFacets.length +
|
|
444
|
+
activeSemanticMemories.length +
|
|
445
|
+
activeProcedural.length +
|
|
446
|
+
cueEpisodeMemories.length +
|
|
447
|
+
cueEpisodeFacets.length +
|
|
448
|
+
cueSemanticMemories.length +
|
|
449
|
+
cueProcedural.length;
|
|
450
|
+
const useLegacyFallback = v2SignalCount === 0;
|
|
451
|
+
const injectedActiveKnowledge = useLegacyFallback ? activeKnowledge : [];
|
|
452
|
+
const injectedActiveLessons = useLegacyFallback ? activeLessons : [];
|
|
453
|
+
const injectedActiveEvents = useLegacyFallback ? activeEvents : [];
|
|
454
|
+
const injectedActivePrefs = useLegacyFallback ? activePrefs : [];
|
|
455
|
+
const injectedCueKnowledge = useLegacyFallback ? dedupedCueKnowledge : [];
|
|
456
|
+
const injectedCueLessons = useLegacyFallback ? dedupedCueLessons : [];
|
|
457
|
+
const injectedCueEvents = useLegacyFallback ? dedupedCueEvents : [];
|
|
458
|
+
const injectedCuePrefs = useLegacyFallback ? dedupedCuePrefs : [];
|
|
459
|
+
const injectedCueEpisodes = useLegacyFallback ? topEpisodes : [];
|
|
418
460
|
// Reinforce all recalled entries (Active + Cue) via spaced repetition
|
|
419
|
-
const allRecalledKnowledge = [...
|
|
420
|
-
const allRecalledLessons = [...
|
|
421
|
-
const allRecalledEvents = [...
|
|
422
|
-
const allRecalledPrefs = [...
|
|
423
|
-
const allRecalledFacets = [...
|
|
461
|
+
const allRecalledKnowledge = [...injectedActiveKnowledge, ...injectedCueKnowledge];
|
|
462
|
+
const allRecalledLessons = [...injectedActiveLessons, ...injectedCueLessons];
|
|
463
|
+
const allRecalledEvents = [...injectedActiveEvents, ...injectedCueEvents];
|
|
464
|
+
const allRecalledPrefs = [...injectedActivePrefs, ...injectedCuePrefs];
|
|
465
|
+
const allRecalledFacets = [...legacyFacetBridgeActive, ...legacyFacetBridgeCue];
|
|
424
466
|
await this.reinforceEntries(allRecalledKnowledge, allKnowledge, this.knowledgePath);
|
|
425
467
|
await this.reinforceEntries(allRecalledLessons, allLessons, this.lessonsPath);
|
|
426
468
|
await this.reinforceEntries(allRecalledEvents, allEvents, this.eventsPath);
|
|
@@ -438,22 +480,22 @@ export class NanoMemEngine {
|
|
|
438
480
|
await this.reconsolidateIfNeeded(allRecalledLessons, contextTags, allLessons);
|
|
439
481
|
}
|
|
440
482
|
return this.buildProgressiveInjectionText({
|
|
441
|
-
knowledge:
|
|
442
|
-
lessons:
|
|
443
|
-
events:
|
|
444
|
-
preferences:
|
|
445
|
-
facets:
|
|
483
|
+
knowledge: injectedActiveKnowledge,
|
|
484
|
+
lessons: injectedActiveLessons,
|
|
485
|
+
events: injectedActiveEvents,
|
|
486
|
+
preferences: injectedActivePrefs,
|
|
487
|
+
facets: legacyFacetBridgeActive,
|
|
446
488
|
episodeMemories: activeEpisodeMemories,
|
|
447
489
|
episodeFacets: activeEpisodeFacets,
|
|
448
490
|
semanticMemories: activeSemanticMemories,
|
|
449
491
|
procedural: activeProcedural,
|
|
450
492
|
}, {
|
|
451
|
-
knowledge:
|
|
452
|
-
lessons:
|
|
453
|
-
events:
|
|
454
|
-
preferences:
|
|
455
|
-
facets:
|
|
456
|
-
episodes:
|
|
493
|
+
knowledge: injectedCueKnowledge,
|
|
494
|
+
lessons: injectedCueLessons,
|
|
495
|
+
events: injectedCueEvents,
|
|
496
|
+
preferences: injectedCuePrefs,
|
|
497
|
+
facets: legacyFacetBridgeCue,
|
|
498
|
+
episodes: injectedCueEpisodes,
|
|
457
499
|
work: topWork,
|
|
458
500
|
episodeMemories: cueEpisodeMemories,
|
|
459
501
|
episodeFacets: cueEpisodeFacets,
|
|
@@ -596,7 +638,7 @@ export class NanoMemEngine {
|
|
|
596
638
|
}
|
|
597
639
|
// ─── Stats ───────────────────────────────────────────────────
|
|
598
640
|
async getStats() {
|
|
599
|
-
const [knowledge, lessons, events, prefs, facets, episodes, work, meta] = await Promise.all([
|
|
641
|
+
const [knowledge, lessons, events, prefs, facets, episodes, work, meta, archived] = await Promise.all([
|
|
600
642
|
loadEntries(this.knowledgePath),
|
|
601
643
|
loadEntries(this.lessonsPath),
|
|
602
644
|
loadEntries(this.eventsPath),
|
|
@@ -605,6 +647,7 @@ export class NanoMemEngine {
|
|
|
605
647
|
loadEpisodes(this.episodesDir),
|
|
606
648
|
loadWork(this.workPath),
|
|
607
649
|
loadMeta(this.metaPath),
|
|
650
|
+
this.exportArchive(),
|
|
608
651
|
]);
|
|
609
652
|
return {
|
|
610
653
|
knowledge: knowledge.length,
|
|
@@ -614,11 +657,17 @@ export class NanoMemEngine {
|
|
|
614
657
|
facets: facets.length,
|
|
615
658
|
episodes: episodes.length,
|
|
616
659
|
work: work.length,
|
|
660
|
+
archivedKnowledge: archived.knowledge.length,
|
|
661
|
+
archivedLessons: archived.lessons.length,
|
|
662
|
+
archivedEvents: archived.events.length,
|
|
663
|
+
archivedPreferences: archived.preferences.length,
|
|
664
|
+
archivedFacets: archived.facets.length,
|
|
665
|
+
archivedWork: archived.work.length,
|
|
617
666
|
totalSessions: meta.totalSessions,
|
|
618
667
|
};
|
|
619
668
|
}
|
|
620
669
|
async getV2Stats() {
|
|
621
|
-
const [episodes, facets, semantic, procedural, links, embeddingIndex, meta] = await Promise.all([
|
|
670
|
+
const [episodes, facets, semantic, procedural, links, embeddingIndex, meta, archived] = await Promise.all([
|
|
622
671
|
loadV2Episodes(this.v2Paths),
|
|
623
672
|
loadV2Facets(this.v2Paths),
|
|
624
673
|
loadV2Semantic(this.v2Paths),
|
|
@@ -626,12 +675,15 @@ export class NanoMemEngine {
|
|
|
626
675
|
loadV2Links(this.v2Paths),
|
|
627
676
|
loadEmbeddingIndex(this.cfg.memoryDir),
|
|
628
677
|
loadV2Meta(this.v2Paths),
|
|
678
|
+
this.exportArchive(),
|
|
629
679
|
]);
|
|
630
680
|
return {
|
|
631
681
|
episodes: episodes.length,
|
|
632
682
|
facets: facets.length,
|
|
633
683
|
semantic: semantic.length,
|
|
634
684
|
procedural: procedural.length,
|
|
685
|
+
archivedSemantic: archived.semantic.length,
|
|
686
|
+
archivedProcedural: archived.procedural.length,
|
|
635
687
|
links: links.length,
|
|
636
688
|
embeddings: embeddingIndex.records.length,
|
|
637
689
|
lastEmbeddingSyncAt: meta.lastEmbeddingSyncAt,
|
|
@@ -648,13 +700,22 @@ export class NanoMemEngine {
|
|
|
648
700
|
facets: await loadEntries(this.facetsPath),
|
|
649
701
|
};
|
|
650
702
|
}
|
|
703
|
+
async getRuntimeIdentityEntries() {
|
|
704
|
+
const runtime = await this.buildRuntimeMemoryView();
|
|
705
|
+
return {
|
|
706
|
+
knowledge: runtime.knowledge,
|
|
707
|
+
lessons: runtime.lessons,
|
|
708
|
+
preferences: runtime.preferences,
|
|
709
|
+
facets: runtime.facets,
|
|
710
|
+
};
|
|
711
|
+
}
|
|
651
712
|
async getAllWork() {
|
|
652
713
|
return loadWork(this.workPath);
|
|
653
714
|
}
|
|
654
715
|
async getAllEpisodes() {
|
|
655
716
|
return loadEpisodes(this.episodesDir);
|
|
656
717
|
}
|
|
657
|
-
async runStartupMaintenance(maintenanceVersion =
|
|
718
|
+
async runStartupMaintenance(maintenanceVersion = 3) {
|
|
658
719
|
const [meta, v2Meta] = await Promise.all([loadMeta(this.metaPath), loadV2Meta(this.v2Paths)]);
|
|
659
720
|
const alreadyMaintained = (meta.lastMaintenanceVersion ?? 0) >= maintenanceVersion &&
|
|
660
721
|
(v2Meta.lastMaintenanceVersion ?? 0) >= maintenanceVersion;
|
|
@@ -664,6 +725,7 @@ export class NanoMemEngine {
|
|
|
664
725
|
backupPath: undefined,
|
|
665
726
|
deduplicated: { knowledge: 0, lessons: 0, events: 0, preferences: 0, facets: 0, work: 0, total: 0 },
|
|
666
727
|
migratedEpisodesToV2: 0,
|
|
728
|
+
archived: { knowledge: 0, lessons: 0, events: 0, preferences: 0, facets: 0, work: 0, semantic: 0, procedural: 0, total: 0 },
|
|
667
729
|
};
|
|
668
730
|
}
|
|
669
731
|
const now = new Date().toISOString();
|
|
@@ -673,6 +735,9 @@ export class NanoMemEngine {
|
|
|
673
735
|
for (const episode of episodes) {
|
|
674
736
|
await this.syncEpisodeToV2(episode);
|
|
675
737
|
}
|
|
738
|
+
await this.rebuildV2Links();
|
|
739
|
+
const archived = await this.archiveStaleMemories();
|
|
740
|
+
await this.rebuildV2Links();
|
|
676
741
|
await Promise.all([
|
|
677
742
|
writeJson(this.metaPath, {
|
|
678
743
|
...(await loadMeta(this.metaPath)),
|
|
@@ -696,6 +761,7 @@ export class NanoMemEngine {
|
|
|
696
761
|
backupPath,
|
|
697
762
|
deduplicated,
|
|
698
763
|
migratedEpisodesToV2: episodes.length,
|
|
764
|
+
archived,
|
|
699
765
|
};
|
|
700
766
|
}
|
|
701
767
|
async createMaintenanceBackup(meta, v2Meta, maintenanceVersion, now) {
|
|
@@ -724,7 +790,7 @@ export class NanoMemEngine {
|
|
|
724
790
|
// Missing files are fine for first-run users.
|
|
725
791
|
}
|
|
726
792
|
}
|
|
727
|
-
for (const dirName of ["episodes", "v2"]) {
|
|
793
|
+
for (const dirName of ["episodes", "v2", "_archive"]) {
|
|
728
794
|
const sourceDir = join(this.cfg.memoryDir, dirName);
|
|
729
795
|
try {
|
|
730
796
|
await readdir(sourceDir);
|
|
@@ -954,23 +1020,108 @@ export class NanoMemEngine {
|
|
|
954
1020
|
};
|
|
955
1021
|
}
|
|
956
1022
|
async searchEntries(query, scope) {
|
|
1023
|
+
await this.autoReviveRelevantArchive(query, scope);
|
|
1024
|
+
const runtime = await this.buildRuntimeMemoryView();
|
|
957
1025
|
const tags = extractTags(query);
|
|
958
|
-
const
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
score
|
|
964
|
-
|
|
1026
|
+
const v2Primary = runtime.v2SearchEntries
|
|
1027
|
+
.map((entry) => ({
|
|
1028
|
+
entry,
|
|
1029
|
+
score: scoreEntry(entry, entry.project, tags, this.cfg.halfLife, this.cfg.scoreWeights),
|
|
1030
|
+
}))
|
|
1031
|
+
.filter((item) => item.score >= 0.42)
|
|
1032
|
+
.sort((a, b) => b.score - a.score)
|
|
1033
|
+
.map((item) => item.entry);
|
|
1034
|
+
if (v2Primary.length > 0) {
|
|
1035
|
+
const bridgeFacets = runtime.facets
|
|
1036
|
+
.filter((entry) => entry.facetData?.kind === "pattern" || entry.facetData?.kind === "struggle")
|
|
1037
|
+
.map((entry) => ({
|
|
1038
|
+
entry,
|
|
1039
|
+
score: scoreEntry(entry, entry.project, tags, this.cfg.halfLife, this.cfg.scoreWeights),
|
|
1040
|
+
}))
|
|
1041
|
+
.filter((item) => item.score >= 0.38)
|
|
1042
|
+
.sort((a, b) => b.score - a.score)
|
|
1043
|
+
.map((item) => item.entry);
|
|
1044
|
+
return [...v2Primary, ...bridgeFacets].slice(0, 12);
|
|
1045
|
+
}
|
|
1046
|
+
const all = [...runtime.knowledge, ...runtime.lessons, ...runtime.events, ...runtime.preferences, ...runtime.facets];
|
|
1047
|
+
return all
|
|
1048
|
+
.map((entry) => ({
|
|
1049
|
+
entry,
|
|
1050
|
+
score: tagOverlap(entry.tags, tags) +
|
|
1051
|
+
(entry.relations ?? [])
|
|
965
1052
|
.filter((relation) => tags.some((tag) => relation.kind.includes(tag) || relation.id.includes(tag)))
|
|
966
1053
|
.reduce((sum, relation) => sum + relation.weight * 0.12, 0),
|
|
967
1054
|
}))
|
|
968
|
-
.filter((
|
|
1055
|
+
.filter((item) => item.score > 0)
|
|
969
1056
|
.sort((a, b) => b.score - a.score)
|
|
970
|
-
.map((
|
|
1057
|
+
.map((item) => item.entry);
|
|
1058
|
+
}
|
|
1059
|
+
async autoReviveRelevantArchive(query, scope, options) {
|
|
1060
|
+
const trimmed = query.trim();
|
|
1061
|
+
const contextTags = extractTags(trimmed);
|
|
1062
|
+
if (!trimmed || contextTags.length === 0)
|
|
1063
|
+
return [];
|
|
1064
|
+
const project = scope?.project ?? "";
|
|
1065
|
+
const hl = this.cfg.halfLife;
|
|
1066
|
+
const sw = this.cfg.scoreWeights;
|
|
1067
|
+
const maxItems = options?.maxItems ?? NanoMemEngine.AUTO_REVIVE_MAX_ITEMS;
|
|
1068
|
+
const legacyThreshold = options?.legacyThreshold ?? 0.82;
|
|
1069
|
+
const v2Threshold = options?.v2Threshold ?? 0.8;
|
|
1070
|
+
const revived = [];
|
|
1071
|
+
const archive = await this.exportArchive();
|
|
1072
|
+
const legacyCandidates = [
|
|
1073
|
+
...archive.knowledge.map((entry) => ({ entry, location: "knowledge" })),
|
|
1074
|
+
...archive.lessons.map((entry) => ({ entry, location: "lessons" })),
|
|
1075
|
+
...archive.events.map((entry) => ({ entry, location: "events" })),
|
|
1076
|
+
...archive.preferences.map((entry) => ({ entry, location: "preferences" })),
|
|
1077
|
+
...archive.facets.map((entry) => ({ entry, location: "facets" })),
|
|
1078
|
+
]
|
|
1079
|
+
.filter(({ entry }) => !scope?.userId || !entry.scope?.userId || entry.scope.userId === scope.userId)
|
|
1080
|
+
.filter(({ entry }) => !scope?.agentId || !entry.scope?.agentId || entry.scope.agentId === scope.agentId)
|
|
1081
|
+
.map(({ entry, location }) => ({
|
|
1082
|
+
id: entry.id,
|
|
1083
|
+
location,
|
|
1084
|
+
score: scoreEntry(entry, project || entry.project, contextTags, hl, sw),
|
|
1085
|
+
}))
|
|
1086
|
+
.filter((candidate) => candidate.score >= legacyThreshold)
|
|
1087
|
+
.sort((a, b) => b.score - a.score)
|
|
1088
|
+
.slice(0, maxItems);
|
|
1089
|
+
for (const candidate of legacyCandidates) {
|
|
1090
|
+
const result = await this.restoreArchivedEntry(candidate.id);
|
|
1091
|
+
if (result.ok && result.location)
|
|
1092
|
+
revived.push({ id: candidate.id, location: result.location });
|
|
1093
|
+
}
|
|
1094
|
+
if (revived.length >= maxItems)
|
|
1095
|
+
return revived;
|
|
1096
|
+
const semanticCandidates = archive.semantic
|
|
1097
|
+
.filter((entry) => !scope?.userId || !entry.scope?.userId || entry.scope.userId === scope.userId)
|
|
1098
|
+
.filter((entry) => !scope?.agentId || !entry.scope?.agentId || entry.scope.agentId === scope.agentId)
|
|
1099
|
+
.filter((entry) => !project || !entry.scope?.project || entry.scope.project === project)
|
|
1100
|
+
.map((entry) => ({ id: entry.id, location: "semantic", score: this.scoreV2SemanticMemory(entry, project, contextTags) }))
|
|
1101
|
+
.filter((candidate) => candidate.score >= v2Threshold);
|
|
1102
|
+
const proceduralCandidates = archive.procedural
|
|
1103
|
+
.filter((entry) => !scope?.userId || !entry.scope?.userId || entry.scope.userId === scope.userId)
|
|
1104
|
+
.filter((entry) => !scope?.agentId || !entry.scope?.agentId || entry.scope.agentId === scope.agentId)
|
|
1105
|
+
.filter((entry) => !project || !entry.scope?.project || entry.scope.project === project)
|
|
1106
|
+
.map((entry) => ({
|
|
1107
|
+
id: entry.id,
|
|
1108
|
+
location: "procedural",
|
|
1109
|
+
// Archived procedures are often older superseded versions, so revive scoring should not punish status as heavily.
|
|
1110
|
+
score: this.scoreProceduralMemory(entry, project, contextTags) + (entry.status === "superseded" || entry.status === "deprecated" ? 0.24 : 0),
|
|
1111
|
+
}))
|
|
1112
|
+
.filter((candidate) => candidate.score >= v2Threshold);
|
|
1113
|
+
const v2Candidates = [...semanticCandidates, ...proceduralCandidates]
|
|
1114
|
+
.sort((a, b) => b.score - a.score)
|
|
1115
|
+
.slice(0, Math.max(0, maxItems - revived.length));
|
|
1116
|
+
for (const candidate of v2Candidates) {
|
|
1117
|
+
const result = await this.restoreArchivedEntry(candidate.id);
|
|
1118
|
+
if (result.ok && result.location)
|
|
1119
|
+
revived.push({ id: candidate.id, location: result.location });
|
|
1120
|
+
}
|
|
1121
|
+
return revived;
|
|
971
1122
|
}
|
|
972
1123
|
async getAlignmentSnapshot() {
|
|
973
|
-
const all = await this.
|
|
1124
|
+
const all = await this.buildRuntimeMemoryView();
|
|
974
1125
|
const memoryEntries = [...all.knowledge, ...all.lessons, ...all.events, ...all.preferences, ...all.facets];
|
|
975
1126
|
const sortByInfluence = (entries) => [...entries].sort((a, b) => {
|
|
976
1127
|
const aScore = (a.salience ?? a.importance) * ((a.accessCount ?? 0) + 1) + (a.retention === "core" ? 2 : 0);
|
|
@@ -1029,6 +1180,19 @@ export class NanoMemEngine {
|
|
|
1029
1180
|
]);
|
|
1030
1181
|
return { knowledge, lessons, events, preferences, facets, work, episodes, meta };
|
|
1031
1182
|
}
|
|
1183
|
+
async exportArchive() {
|
|
1184
|
+
const [knowledge, lessons, events, preferences, facets, work, semantic, procedural] = await Promise.all([
|
|
1185
|
+
loadEntries(this.archiveKnowledgePath),
|
|
1186
|
+
loadEntries(this.archiveLessonsPath),
|
|
1187
|
+
loadEntries(this.archiveEventsPath),
|
|
1188
|
+
loadEntries(this.archivePreferencesPath),
|
|
1189
|
+
loadEntries(this.archiveFacetsPath),
|
|
1190
|
+
loadWork(this.archiveWorkPath),
|
|
1191
|
+
loadV2Semantic(this.archiveV2Paths),
|
|
1192
|
+
loadV2Procedural(this.archiveV2Paths),
|
|
1193
|
+
]);
|
|
1194
|
+
return { knowledge, lessons, events, preferences, facets, work, semantic, procedural };
|
|
1195
|
+
}
|
|
1032
1196
|
async exportAllV2() {
|
|
1033
1197
|
const [episodes, facets, semantic, procedural, links] = await Promise.all([
|
|
1034
1198
|
loadV2Episodes(this.v2Paths),
|
|
@@ -1039,16 +1203,206 @@ export class NanoMemEngine {
|
|
|
1039
1203
|
]);
|
|
1040
1204
|
return { episodes, facets, semantic, procedural, links };
|
|
1041
1205
|
}
|
|
1206
|
+
async rebuildV2Links() {
|
|
1207
|
+
const snapshot = await this.exportAllV2();
|
|
1208
|
+
const nextLinks = this.materializeV2Links(snapshot.semantic, snapshot.procedural, snapshot.links);
|
|
1209
|
+
await saveV2Links(this.v2Paths, nextLinks);
|
|
1210
|
+
return {
|
|
1211
|
+
total: nextLinks.length,
|
|
1212
|
+
auto: nextLinks.filter((link) => link.id.startsWith(NanoMemEngine.AUTO_V2_LINK_PREFIX)).length,
|
|
1213
|
+
};
|
|
1214
|
+
}
|
|
1215
|
+
async archiveStaleMemories(now = new Date().toISOString()) {
|
|
1216
|
+
const [knowledge, lessons, events, preferences, facets, work, archived, semantic, procedural, archivedSemantic, archivedProcedural] = await Promise.all([
|
|
1217
|
+
loadEntries(this.knowledgePath),
|
|
1218
|
+
loadEntries(this.lessonsPath),
|
|
1219
|
+
loadEntries(this.eventsPath),
|
|
1220
|
+
loadEntries(this.preferencesPath),
|
|
1221
|
+
loadEntries(this.facetsPath),
|
|
1222
|
+
loadWork(this.workPath),
|
|
1223
|
+
this.exportArchive(),
|
|
1224
|
+
loadV2Semantic(this.v2Paths),
|
|
1225
|
+
loadV2Procedural(this.v2Paths),
|
|
1226
|
+
loadV2Semantic(this.archiveV2Paths),
|
|
1227
|
+
loadV2Procedural(this.archiveV2Paths),
|
|
1228
|
+
]);
|
|
1229
|
+
const knowledgeResult = this.partitionArchivedEntries(knowledge, now);
|
|
1230
|
+
const lessonsResult = this.partitionArchivedEntries(lessons, now);
|
|
1231
|
+
const eventsResult = this.partitionArchivedEntries(events, now);
|
|
1232
|
+
const preferencesResult = this.partitionArchivedEntries(preferences, now);
|
|
1233
|
+
const facetsResult = this.partitionArchivedEntries(facets, now);
|
|
1234
|
+
const workResult = this.partitionArchivedWork(work, now);
|
|
1235
|
+
const semanticResult = this.partitionArchivedSemantic(semantic, now);
|
|
1236
|
+
const proceduralResult = this.partitionArchivedProcedural(procedural, now);
|
|
1237
|
+
const hl = this.cfg.halfLife;
|
|
1238
|
+
const ew = this.cfg.evictionWeights;
|
|
1239
|
+
await Promise.all([
|
|
1240
|
+
saveEntries(this.knowledgePath, knowledgeResult.active, this.cfg.maxEntries.knowledge, (entry) => utilityEntry(entry, hl, ew)),
|
|
1241
|
+
saveEntries(this.lessonsPath, lessonsResult.active, this.cfg.maxEntries.lessons, (entry) => utilityEntry(entry, hl, ew)),
|
|
1242
|
+
saveEntries(this.eventsPath, eventsResult.active, this.cfg.maxEntries.events, (entry) => utilityEntry(entry, hl, ew)),
|
|
1243
|
+
saveEntries(this.preferencesPath, preferencesResult.active, this.cfg.maxEntries.preferences, (entry) => utilityEntry(entry, hl, ew)),
|
|
1244
|
+
saveEntries(this.facetsPath, facetsResult.active, this.cfg.maxEntries.facets, (entry) => utilityEntry(entry, hl, ew)),
|
|
1245
|
+
saveWork(this.workPath, workResult.active, this.cfg.maxEntries.work, (entry) => utilityWork(entry, this.cfg.halfLife, this.cfg.evictionWeights)),
|
|
1246
|
+
saveEntries(this.archiveKnowledgePath, this.mergeArchivedEntries(archived.knowledge, knowledgeResult.archived), Infinity, (entry) => utilityEntry(entry, hl, ew)),
|
|
1247
|
+
saveEntries(this.archiveLessonsPath, this.mergeArchivedEntries(archived.lessons, lessonsResult.archived), Infinity, (entry) => utilityEntry(entry, hl, ew)),
|
|
1248
|
+
saveEntries(this.archiveEventsPath, this.mergeArchivedEntries(archived.events, eventsResult.archived), Infinity, (entry) => utilityEntry(entry, hl, ew)),
|
|
1249
|
+
saveEntries(this.archivePreferencesPath, this.mergeArchivedEntries(archived.preferences, preferencesResult.archived), Infinity, (entry) => utilityEntry(entry, hl, ew)),
|
|
1250
|
+
saveEntries(this.archiveFacetsPath, this.mergeArchivedEntries(archived.facets, facetsResult.archived), Infinity, (entry) => utilityEntry(entry, hl, ew)),
|
|
1251
|
+
saveWork(this.archiveWorkPath, this.mergeArchivedWork(archived.work, workResult.archived), Infinity, (entry) => utilityWork(entry, this.cfg.halfLife, this.cfg.evictionWeights)),
|
|
1252
|
+
saveV2Semantic(this.v2Paths, semanticResult.active),
|
|
1253
|
+
saveV2Procedural(this.v2Paths, proceduralResult.active),
|
|
1254
|
+
saveV2Semantic(this.archiveV2Paths, this.mergeArchivedV2(archivedSemantic, semanticResult.archived)),
|
|
1255
|
+
saveV2Procedural(this.archiveV2Paths, this.mergeArchivedV2(archivedProcedural, proceduralResult.archived)),
|
|
1256
|
+
]);
|
|
1257
|
+
const total = knowledgeResult.archived.length +
|
|
1258
|
+
lessonsResult.archived.length +
|
|
1259
|
+
eventsResult.archived.length +
|
|
1260
|
+
preferencesResult.archived.length +
|
|
1261
|
+
facetsResult.archived.length +
|
|
1262
|
+
workResult.archived.length +
|
|
1263
|
+
semanticResult.archived.length +
|
|
1264
|
+
proceduralResult.archived.length;
|
|
1265
|
+
return {
|
|
1266
|
+
knowledge: knowledgeResult.archived.length,
|
|
1267
|
+
lessons: lessonsResult.archived.length,
|
|
1268
|
+
events: eventsResult.archived.length,
|
|
1269
|
+
preferences: preferencesResult.archived.length,
|
|
1270
|
+
facets: facetsResult.archived.length,
|
|
1271
|
+
work: workResult.archived.length,
|
|
1272
|
+
semantic: semanticResult.archived.length,
|
|
1273
|
+
procedural: proceduralResult.archived.length,
|
|
1274
|
+
total,
|
|
1275
|
+
};
|
|
1276
|
+
}
|
|
1277
|
+
async restoreArchivedEntry(id) {
|
|
1278
|
+
const hl = this.cfg.halfLife;
|
|
1279
|
+
const ew = this.cfg.evictionWeights;
|
|
1280
|
+
const restoreLegacy = async (activePath, archivePath, location) => {
|
|
1281
|
+
const [active, archived] = await Promise.all([loadEntries(activePath), loadEntries(archivePath)]);
|
|
1282
|
+
const index = archived.findIndex((entry) => entry.id === id);
|
|
1283
|
+
if (index < 0)
|
|
1284
|
+
return { ok: false };
|
|
1285
|
+
const [entry] = archived.splice(index, 1);
|
|
1286
|
+
active.push({
|
|
1287
|
+
...entry,
|
|
1288
|
+
archivedAt: undefined,
|
|
1289
|
+
archiveReason: undefined,
|
|
1290
|
+
revivedAt: new Date().toISOString(),
|
|
1291
|
+
});
|
|
1292
|
+
await Promise.all([
|
|
1293
|
+
saveEntries(activePath, active, Infinity, (candidate) => utilityEntry(candidate, hl, ew)),
|
|
1294
|
+
saveEntries(archivePath, archived, Infinity, (candidate) => utilityEntry(candidate, hl, ew)),
|
|
1295
|
+
]);
|
|
1296
|
+
return { ok: true, location };
|
|
1297
|
+
};
|
|
1298
|
+
for (const [activePath, archivePath, location] of [
|
|
1299
|
+
[this.knowledgePath, this.archiveKnowledgePath, "knowledge"],
|
|
1300
|
+
[this.lessonsPath, this.archiveLessonsPath, "lessons"],
|
|
1301
|
+
[this.eventsPath, this.archiveEventsPath, "events"],
|
|
1302
|
+
[this.preferencesPath, this.archivePreferencesPath, "preferences"],
|
|
1303
|
+
[this.facetsPath, this.archiveFacetsPath, "facets"],
|
|
1304
|
+
]) {
|
|
1305
|
+
const result = await restoreLegacy(activePath, archivePath, location);
|
|
1306
|
+
if (result.ok)
|
|
1307
|
+
return result;
|
|
1308
|
+
}
|
|
1309
|
+
{
|
|
1310
|
+
const [active, archived] = await Promise.all([loadWork(this.workPath), loadWork(this.archiveWorkPath)]);
|
|
1311
|
+
const index = archived.findIndex((entry) => entry.id === id);
|
|
1312
|
+
if (index >= 0) {
|
|
1313
|
+
const [entry] = archived.splice(index, 1);
|
|
1314
|
+
active.push({
|
|
1315
|
+
...entry,
|
|
1316
|
+
archivedAt: undefined,
|
|
1317
|
+
archiveReason: undefined,
|
|
1318
|
+
revivedAt: new Date().toISOString(),
|
|
1319
|
+
});
|
|
1320
|
+
await Promise.all([
|
|
1321
|
+
saveWork(this.workPath, active, Infinity, (candidate) => utilityWork(candidate, this.cfg.halfLife, this.cfg.evictionWeights)),
|
|
1322
|
+
saveWork(this.archiveWorkPath, archived, Infinity, (candidate) => utilityWork(candidate, this.cfg.halfLife, this.cfg.evictionWeights)),
|
|
1323
|
+
]);
|
|
1324
|
+
return { ok: true, location: "work" };
|
|
1325
|
+
}
|
|
1326
|
+
}
|
|
1327
|
+
{
|
|
1328
|
+
const [active, archived] = await Promise.all([loadV2Semantic(this.v2Paths), loadV2Semantic(this.archiveV2Paths)]);
|
|
1329
|
+
const index = archived.findIndex((entry) => entry.id === id);
|
|
1330
|
+
if (index >= 0) {
|
|
1331
|
+
const [entry] = archived.splice(index, 1);
|
|
1332
|
+
active.push({
|
|
1333
|
+
...entry,
|
|
1334
|
+
archivedAt: undefined,
|
|
1335
|
+
archiveReason: undefined,
|
|
1336
|
+
revivedAt: new Date().toISOString(),
|
|
1337
|
+
});
|
|
1338
|
+
await Promise.all([saveV2Semantic(this.v2Paths, active), saveV2Semantic(this.archiveV2Paths, archived)]);
|
|
1339
|
+
await this.rebuildV2Links();
|
|
1340
|
+
return { ok: true, location: "semantic" };
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
{
|
|
1344
|
+
const [active, archived] = await Promise.all([loadV2Procedural(this.v2Paths), loadV2Procedural(this.archiveV2Paths)]);
|
|
1345
|
+
const index = archived.findIndex((entry) => entry.id === id);
|
|
1346
|
+
if (index >= 0) {
|
|
1347
|
+
const [entry] = archived.splice(index, 1);
|
|
1348
|
+
active.push({
|
|
1349
|
+
...entry,
|
|
1350
|
+
archivedAt: undefined,
|
|
1351
|
+
archiveReason: undefined,
|
|
1352
|
+
revivedAt: new Date().toISOString(),
|
|
1353
|
+
});
|
|
1354
|
+
await Promise.all([saveV2Procedural(this.v2Paths, active), saveV2Procedural(this.archiveV2Paths, archived)]);
|
|
1355
|
+
await this.rebuildV2Links();
|
|
1356
|
+
return { ok: true, location: "procedural" };
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
return { ok: false };
|
|
1360
|
+
}
|
|
1361
|
+
async inspectV2Memory(project = "", scope) {
|
|
1362
|
+
const snapshot = await this.exportAllV2();
|
|
1363
|
+
const episodes = this.filterAndCleanV2Episodes(snapshot.episodes, project, scope);
|
|
1364
|
+
const facets = this.filterAndCleanV2Facets(snapshot.facets, project, scope);
|
|
1365
|
+
const semantic = this.filterAndCleanV2Semantic(snapshot.semantic, project, scope);
|
|
1366
|
+
const allProcedural = snapshot.procedural.filter((entry) => {
|
|
1367
|
+
if (scope?.userId && entry.scope?.userId && entry.scope.userId !== scope.userId)
|
|
1368
|
+
return false;
|
|
1369
|
+
if (scope?.agentId && entry.scope?.agentId && entry.scope.agentId !== scope.agentId)
|
|
1370
|
+
return false;
|
|
1371
|
+
if (project && entry.scope?.project && entry.scope.project !== project)
|
|
1372
|
+
return false;
|
|
1373
|
+
return true;
|
|
1374
|
+
});
|
|
1375
|
+
const activeProcedural = allProcedural.filter((entry) => entry.status !== "deprecated" && entry.status !== "superseded");
|
|
1376
|
+
const procedureChains = this.buildProceduralChains(allProcedural);
|
|
1377
|
+
const proceduralConflicts = this.detectProceduralConflicts(activeProcedural);
|
|
1378
|
+
const semanticConflicts = this.detectSemanticConflicts(semantic);
|
|
1379
|
+
return {
|
|
1380
|
+
counts: {
|
|
1381
|
+
episodes: episodes.length,
|
|
1382
|
+
facets: facets.length,
|
|
1383
|
+
semantic: semantic.length,
|
|
1384
|
+
procedural: allProcedural.length,
|
|
1385
|
+
activeProcedural: activeProcedural.length,
|
|
1386
|
+
supersededProcedural: allProcedural.filter((entry) => entry.status === "superseded").length,
|
|
1387
|
+
semanticConflicts: semanticConflicts.length,
|
|
1388
|
+
proceduralConflicts: proceduralConflicts.length,
|
|
1389
|
+
procedureChains: procedureChains.length,
|
|
1390
|
+
},
|
|
1391
|
+
procedureChains,
|
|
1392
|
+
proceduralConflicts,
|
|
1393
|
+
semanticConflicts,
|
|
1394
|
+
};
|
|
1395
|
+
}
|
|
1042
1396
|
// ─── Full Insights Report ────────────────────────────────────────
|
|
1043
1397
|
async generateFullInsights() {
|
|
1044
|
-
const all = await this.
|
|
1398
|
+
const all = await this.buildRuntimeMemoryView();
|
|
1045
1399
|
return buildFullInsightsReport(all, this.llmFn, this.cfg.locale);
|
|
1046
1400
|
}
|
|
1047
1401
|
/**
|
|
1048
1402
|
* 生成增强版洞察报告(包含大白话洞察 + 开发者画像 + 根因分析)
|
|
1049
1403
|
*/
|
|
1050
1404
|
async generateEnhancedInsights() {
|
|
1051
|
-
const all = await this.
|
|
1405
|
+
const all = await this.buildRuntimeMemoryView();
|
|
1052
1406
|
// 并行生成基础报告和大白话洞察
|
|
1053
1407
|
const [baseReport, humanData] = await Promise.all([
|
|
1054
1408
|
buildFullInsightsReport(all, this.llmFn, this.cfg.locale),
|
|
@@ -1063,7 +1417,7 @@ export class NanoMemEngine {
|
|
|
1063
1417
|
}
|
|
1064
1418
|
// ─── Insights Generation ─────────────────────────────────────
|
|
1065
1419
|
async generateInsights() {
|
|
1066
|
-
const all = await this.
|
|
1420
|
+
const all = await this.buildRuntimeMemoryView();
|
|
1067
1421
|
const stats = {
|
|
1068
1422
|
knowledge: all.knowledge.length,
|
|
1069
1423
|
lessons: all.lessons.length,
|
|
@@ -1219,10 +1573,10 @@ export class NanoMemEngine {
|
|
|
1219
1573
|
await saveEntries(path, entries, max, (entry) => utilityEntry(entry, hl, ew));
|
|
1220
1574
|
}
|
|
1221
1575
|
filterAndCleanEntries(entries, scope) {
|
|
1222
|
-
return filterByScope(evictExpiredEntries(entries), scope);
|
|
1576
|
+
return filterByScope(evictExpiredEntries(entries).filter((entry) => !entry.archivedAt), scope);
|
|
1223
1577
|
}
|
|
1224
1578
|
filterAndCleanWork(entries, scope) {
|
|
1225
|
-
return filterByScope(evictExpiredWork(entries), scope);
|
|
1579
|
+
return filterByScope(evictExpiredWork(entries).filter((entry) => !entry.archivedAt), scope);
|
|
1226
1580
|
}
|
|
1227
1581
|
filterAndCleanProcedural(entries, project, scope) {
|
|
1228
1582
|
return entries.filter((entry) => {
|
|
@@ -1232,7 +1586,7 @@ export class NanoMemEngine {
|
|
|
1232
1586
|
return false;
|
|
1233
1587
|
if (project && entry.scope?.project && entry.scope.project !== project)
|
|
1234
1588
|
return false;
|
|
1235
|
-
return entry.status !== "deprecated" && entry.status !== "superseded";
|
|
1589
|
+
return !entry.archivedAt && entry.status !== "deprecated" && entry.status !== "superseded";
|
|
1236
1590
|
});
|
|
1237
1591
|
}
|
|
1238
1592
|
filterAndCleanV2Episodes(entries, project, scope) {
|
|
@@ -1243,7 +1597,7 @@ export class NanoMemEngine {
|
|
|
1243
1597
|
return false;
|
|
1244
1598
|
if (project && entry.scope?.project && entry.scope.project !== project)
|
|
1245
1599
|
return false;
|
|
1246
|
-
return
|
|
1600
|
+
return !entry.archivedAt;
|
|
1247
1601
|
});
|
|
1248
1602
|
}
|
|
1249
1603
|
filterAndCleanV2Facets(entries, project, scope) {
|
|
@@ -1254,7 +1608,7 @@ export class NanoMemEngine {
|
|
|
1254
1608
|
return false;
|
|
1255
1609
|
if (project && entry.scope?.project && entry.scope.project !== project)
|
|
1256
1610
|
return false;
|
|
1257
|
-
return
|
|
1611
|
+
return !entry.archivedAt;
|
|
1258
1612
|
});
|
|
1259
1613
|
}
|
|
1260
1614
|
filterAndCleanV2Semantic(entries, project, scope) {
|
|
@@ -1265,9 +1619,498 @@ export class NanoMemEngine {
|
|
|
1265
1619
|
return false;
|
|
1266
1620
|
if (project && entry.scope?.project && entry.scope.project !== project)
|
|
1267
1621
|
return false;
|
|
1268
|
-
return !entry.supersededById;
|
|
1622
|
+
return !entry.archivedAt && !entry.supersededById;
|
|
1269
1623
|
});
|
|
1270
1624
|
}
|
|
1625
|
+
mergeArchivedEntries(existing, incoming) {
|
|
1626
|
+
const merged = new Map(existing.map((entry) => [entry.id, entry]));
|
|
1627
|
+
for (const entry of incoming)
|
|
1628
|
+
merged.set(entry.id, entry);
|
|
1629
|
+
return [...merged.values()].sort((a, b) => (b.archivedAt ?? b.created).localeCompare(a.archivedAt ?? a.created));
|
|
1630
|
+
}
|
|
1631
|
+
mergeArchivedWork(existing, incoming) {
|
|
1632
|
+
const merged = new Map(existing.map((entry) => [entry.id, entry]));
|
|
1633
|
+
for (const entry of incoming)
|
|
1634
|
+
merged.set(entry.id, entry);
|
|
1635
|
+
return [...merged.values()].sort((a, b) => (b.archivedAt ?? b.created).localeCompare(a.archivedAt ?? a.created));
|
|
1636
|
+
}
|
|
1637
|
+
mergeArchivedV2(existing, incoming) {
|
|
1638
|
+
const merged = new Map(existing.map((entry) => [entry.id, entry]));
|
|
1639
|
+
for (const entry of incoming)
|
|
1640
|
+
merged.set(entry.id, entry);
|
|
1641
|
+
return [...merged.values()].sort((a, b) => (b.archivedAt ?? b.createdAt).localeCompare(a.archivedAt ?? a.createdAt));
|
|
1642
|
+
}
|
|
1643
|
+
partitionArchivedEntries(entries, now) {
|
|
1644
|
+
const active = [];
|
|
1645
|
+
const archived = [];
|
|
1646
|
+
for (const entry of entries) {
|
|
1647
|
+
const reason = this.getLegacyArchiveReason(entry);
|
|
1648
|
+
if (!reason) {
|
|
1649
|
+
active.push(entry);
|
|
1650
|
+
continue;
|
|
1651
|
+
}
|
|
1652
|
+
archived.push({
|
|
1653
|
+
...entry,
|
|
1654
|
+
archivedAt: entry.archivedAt ?? now,
|
|
1655
|
+
archiveReason: entry.archiveReason ?? reason,
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
return { active, archived };
|
|
1659
|
+
}
|
|
1660
|
+
partitionArchivedWork(entries, now) {
|
|
1661
|
+
const active = [];
|
|
1662
|
+
const archived = [];
|
|
1663
|
+
for (const entry of entries) {
|
|
1664
|
+
const reason = this.getWorkArchiveReason(entry);
|
|
1665
|
+
if (!reason) {
|
|
1666
|
+
active.push(entry);
|
|
1667
|
+
continue;
|
|
1668
|
+
}
|
|
1669
|
+
archived.push({
|
|
1670
|
+
...entry,
|
|
1671
|
+
archivedAt: entry.archivedAt ?? now,
|
|
1672
|
+
archiveReason: entry.archiveReason ?? reason,
|
|
1673
|
+
});
|
|
1674
|
+
}
|
|
1675
|
+
return { active, archived };
|
|
1676
|
+
}
|
|
1677
|
+
partitionArchivedSemantic(entries, now) {
|
|
1678
|
+
const active = [];
|
|
1679
|
+
const archived = [];
|
|
1680
|
+
for (const entry of entries) {
|
|
1681
|
+
const reason = this.getSemanticArchiveReason(entry);
|
|
1682
|
+
if (!reason) {
|
|
1683
|
+
active.push(entry);
|
|
1684
|
+
continue;
|
|
1685
|
+
}
|
|
1686
|
+
archived.push({
|
|
1687
|
+
...entry,
|
|
1688
|
+
archivedAt: entry.archivedAt ?? now,
|
|
1689
|
+
archiveReason: entry.archiveReason ?? reason,
|
|
1690
|
+
});
|
|
1691
|
+
}
|
|
1692
|
+
return { active, archived };
|
|
1693
|
+
}
|
|
1694
|
+
partitionArchivedProcedural(entries, now) {
|
|
1695
|
+
const active = [];
|
|
1696
|
+
const archived = [];
|
|
1697
|
+
for (const entry of entries) {
|
|
1698
|
+
const reason = this.getProceduralArchiveReason(entry);
|
|
1699
|
+
if (!reason) {
|
|
1700
|
+
active.push(entry);
|
|
1701
|
+
continue;
|
|
1702
|
+
}
|
|
1703
|
+
archived.push({
|
|
1704
|
+
...entry,
|
|
1705
|
+
archivedAt: entry.archivedAt ?? now,
|
|
1706
|
+
archiveReason: entry.archiveReason ?? reason,
|
|
1707
|
+
});
|
|
1708
|
+
}
|
|
1709
|
+
return { active, archived };
|
|
1710
|
+
}
|
|
1711
|
+
getLegacyArchiveReason(entry) {
|
|
1712
|
+
if (entry.archivedAt)
|
|
1713
|
+
return entry.archiveReason ?? "pre-archived";
|
|
1714
|
+
if (entry.revivedAt && daysSince(entry.revivedAt) <= this.cfg.forgetting.reviveCooldownDays)
|
|
1715
|
+
return undefined;
|
|
1716
|
+
const anchor = entry.lastAccessed ?? entry.eventTime ?? entry.created;
|
|
1717
|
+
const ageDays = daysSince(anchor);
|
|
1718
|
+
const lowSignal = (entry.accessCount ?? 0) <= 1 && entry.importance <= 6 && (entry.salience ?? entry.importance) <= 6;
|
|
1719
|
+
if (entry.retention === "ambient" && lowSignal && ageDays > this.cfg.forgetting.ambientTtlDays) {
|
|
1720
|
+
return "stale-ambient-memory";
|
|
1721
|
+
}
|
|
1722
|
+
return undefined;
|
|
1723
|
+
}
|
|
1724
|
+
inferSemanticRetention(item) {
|
|
1725
|
+
if (item.retention)
|
|
1726
|
+
return item.retention;
|
|
1727
|
+
if (item.stability === "situational" || item.stateData)
|
|
1728
|
+
return "ambient";
|
|
1729
|
+
switch (item.type) {
|
|
1730
|
+
case "preference":
|
|
1731
|
+
case "pattern":
|
|
1732
|
+
return "core";
|
|
1733
|
+
case "lesson":
|
|
1734
|
+
case "decision":
|
|
1735
|
+
case "event":
|
|
1736
|
+
case "struggle":
|
|
1737
|
+
return "key-event";
|
|
1738
|
+
default:
|
|
1739
|
+
return "ambient";
|
|
1740
|
+
}
|
|
1741
|
+
}
|
|
1742
|
+
inferSemanticStability(item) {
|
|
1743
|
+
if (item.stability === "situational")
|
|
1744
|
+
return "situational";
|
|
1745
|
+
if (item.stateData)
|
|
1746
|
+
return "volatile";
|
|
1747
|
+
switch (item.type) {
|
|
1748
|
+
case "preference":
|
|
1749
|
+
case "pattern":
|
|
1750
|
+
return "stable";
|
|
1751
|
+
case "event":
|
|
1752
|
+
case "struggle":
|
|
1753
|
+
return "situational";
|
|
1754
|
+
default:
|
|
1755
|
+
return "stable";
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
inferSemanticImportance(item) {
|
|
1759
|
+
switch (item.type) {
|
|
1760
|
+
case "struggle":
|
|
1761
|
+
case "event":
|
|
1762
|
+
return 9;
|
|
1763
|
+
case "lesson":
|
|
1764
|
+
case "decision":
|
|
1765
|
+
return 8;
|
|
1766
|
+
case "pattern":
|
|
1767
|
+
return 7;
|
|
1768
|
+
case "preference":
|
|
1769
|
+
return 6;
|
|
1770
|
+
default:
|
|
1771
|
+
return 5;
|
|
1772
|
+
}
|
|
1773
|
+
}
|
|
1774
|
+
mapExtractedItemToSemanticType(item) {
|
|
1775
|
+
switch (item.type) {
|
|
1776
|
+
case "lesson":
|
|
1777
|
+
return "lesson";
|
|
1778
|
+
case "preference":
|
|
1779
|
+
return "preference";
|
|
1780
|
+
case "decision":
|
|
1781
|
+
return "decision";
|
|
1782
|
+
case "pattern":
|
|
1783
|
+
return "pattern";
|
|
1784
|
+
case "struggle":
|
|
1785
|
+
return "struggle";
|
|
1786
|
+
case "event":
|
|
1787
|
+
return "event";
|
|
1788
|
+
default:
|
|
1789
|
+
return "fact";
|
|
1790
|
+
}
|
|
1791
|
+
}
|
|
1792
|
+
upsertSemanticFromExtractedItem(semantic, item, project) {
|
|
1793
|
+
if (item.type === "retract")
|
|
1794
|
+
return;
|
|
1795
|
+
const detail = filterPII(item.detail || item.content || "");
|
|
1796
|
+
const name = item.name || detail.slice(0, 30) || item.summary || "memory";
|
|
1797
|
+
const summary = item.summary || detail.slice(0, 150) || name;
|
|
1798
|
+
const semanticType = this.mapExtractedItemToSemanticType(item);
|
|
1799
|
+
const tags = extractTags(`${name} ${summary} ${detail}`);
|
|
1800
|
+
const now = new Date().toISOString();
|
|
1801
|
+
const existing = semantic.find((entry) => entry.semanticType === semanticType &&
|
|
1802
|
+
entry.scope?.project === project &&
|
|
1803
|
+
(tagOverlap(entry.tags, tags) >= 0.72 ||
|
|
1804
|
+
`${entry.name} ${entry.summary}`.trim().toLowerCase() === `${name} ${summary}`.trim().toLowerCase()));
|
|
1805
|
+
if (existing) {
|
|
1806
|
+
existing.name = name;
|
|
1807
|
+
existing.summary = summary;
|
|
1808
|
+
existing.detail = detail || existing.detail;
|
|
1809
|
+
existing.tags = [...new Set(tags)];
|
|
1810
|
+
existing.updatedAt = now;
|
|
1811
|
+
existing.retention = this.inferSemanticRetention(item);
|
|
1812
|
+
existing.stability = this.inferSemanticStability(item);
|
|
1813
|
+
existing.salience = Math.max(existing.salience, this.inferSemanticImportance(item));
|
|
1814
|
+
existing.importance = Math.max(existing.importance, this.inferSemanticImportance(item));
|
|
1815
|
+
return;
|
|
1816
|
+
}
|
|
1817
|
+
semantic.push({
|
|
1818
|
+
id: `semantic:extract:${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
1819
|
+
kind: "semantic",
|
|
1820
|
+
semanticType,
|
|
1821
|
+
name,
|
|
1822
|
+
summary,
|
|
1823
|
+
detail,
|
|
1824
|
+
accessCount: 0,
|
|
1825
|
+
importance: this.inferSemanticImportance(item),
|
|
1826
|
+
salience: Math.max(4, this.inferSemanticImportance(item)),
|
|
1827
|
+
confidence: 0.8,
|
|
1828
|
+
retention: this.inferSemanticRetention(item),
|
|
1829
|
+
stability: this.inferSemanticStability(item),
|
|
1830
|
+
tags,
|
|
1831
|
+
evidence: [],
|
|
1832
|
+
scope: { ...this.cfg.defaultScope, project },
|
|
1833
|
+
createdAt: now,
|
|
1834
|
+
updatedAt: now,
|
|
1835
|
+
abstractionLevel: semanticType === "event" ? "instance" : "generalization",
|
|
1836
|
+
});
|
|
1837
|
+
}
|
|
1838
|
+
semanticKindToLegacyType(kind) {
|
|
1839
|
+
switch (kind) {
|
|
1840
|
+
case "lesson":
|
|
1841
|
+
return "lesson";
|
|
1842
|
+
case "preference":
|
|
1843
|
+
return "preference";
|
|
1844
|
+
case "decision":
|
|
1845
|
+
return "decision";
|
|
1846
|
+
case "event":
|
|
1847
|
+
return "event";
|
|
1848
|
+
case "pattern":
|
|
1849
|
+
return "pattern";
|
|
1850
|
+
case "struggle":
|
|
1851
|
+
return "struggle";
|
|
1852
|
+
default:
|
|
1853
|
+
return "fact";
|
|
1854
|
+
}
|
|
1855
|
+
}
|
|
1856
|
+
semanticToRuntimeEntry(entry) {
|
|
1857
|
+
const type = this.semanticKindToLegacyType(entry.semanticType);
|
|
1858
|
+
return {
|
|
1859
|
+
id: entry.id,
|
|
1860
|
+
type,
|
|
1861
|
+
name: entry.name,
|
|
1862
|
+
summary: entry.summary,
|
|
1863
|
+
detail: entry.detail,
|
|
1864
|
+
content: entry.detail || entry.summary,
|
|
1865
|
+
tags: entry.tags,
|
|
1866
|
+
project: entry.scope?.project || "default",
|
|
1867
|
+
importance: entry.importance,
|
|
1868
|
+
strength: undefined,
|
|
1869
|
+
created: entry.createdAt,
|
|
1870
|
+
eventTime: entry.validFrom ?? entry.createdAt,
|
|
1871
|
+
lastAccessed: entry.lastAccessedAt,
|
|
1872
|
+
accessCount: entry.accessCount,
|
|
1873
|
+
relatedIds: [...new Set([...(entry.supersedesIds ?? []), ...(entry.conflictWithIds ?? [])])],
|
|
1874
|
+
scope: entry.scope,
|
|
1875
|
+
retention: entry.retention === "ambient" ? "ambient" : entry.retention === "core" ? "core" : "key-event",
|
|
1876
|
+
salience: entry.salience,
|
|
1877
|
+
stability: entry.stability === "volatile" ? "situational" : entry.stability === "stable" ? "stable" : "situational",
|
|
1878
|
+
};
|
|
1879
|
+
}
|
|
1880
|
+
proceduralToRuntimeEntry(entry) {
|
|
1881
|
+
return {
|
|
1882
|
+
id: entry.id,
|
|
1883
|
+
type: "decision",
|
|
1884
|
+
name: entry.name,
|
|
1885
|
+
summary: entry.summary,
|
|
1886
|
+
detail: [entry.contextText, entry.boundaries, entry.steps.map((step) => step.text).join("\n")].filter(Boolean).join("\n"),
|
|
1887
|
+
content: entry.summary,
|
|
1888
|
+
tags: entry.tags,
|
|
1889
|
+
project: entry.scope?.project || "default",
|
|
1890
|
+
importance: entry.importance,
|
|
1891
|
+
strength: undefined,
|
|
1892
|
+
created: entry.createdAt,
|
|
1893
|
+
eventTime: entry.validFrom ?? entry.createdAt,
|
|
1894
|
+
lastAccessed: entry.lastAccessedAt,
|
|
1895
|
+
accessCount: entry.accessCount,
|
|
1896
|
+
relatedIds: entry.supersedesIds,
|
|
1897
|
+
scope: entry.scope,
|
|
1898
|
+
retention: entry.retention === "ambient" ? "ambient" : entry.retention === "core" ? "core" : "key-event",
|
|
1899
|
+
salience: entry.salience,
|
|
1900
|
+
stability: entry.stability === "volatile" ? "situational" : entry.stability === "stable" ? "stable" : "situational",
|
|
1901
|
+
};
|
|
1902
|
+
}
|
|
1903
|
+
async buildRuntimeMemoryView() {
|
|
1904
|
+
const [legacy, work, episodes, meta, semantic, procedural] = await Promise.all([
|
|
1905
|
+
this.exportAll(),
|
|
1906
|
+
this.getAllWork(),
|
|
1907
|
+
this.getAllEpisodes(),
|
|
1908
|
+
loadMeta(this.metaPath),
|
|
1909
|
+
loadV2Semantic(this.v2Paths),
|
|
1910
|
+
loadV2Procedural(this.v2Paths),
|
|
1911
|
+
]);
|
|
1912
|
+
const cleanSemantic = semantic.filter((entry) => !entry.archivedAt && !entry.supersededById);
|
|
1913
|
+
const cleanProcedural = procedural.filter((entry) => !entry.archivedAt && entry.status !== "deprecated");
|
|
1914
|
+
const semanticEntries = cleanSemantic.map((entry) => this.semanticToRuntimeEntry(entry));
|
|
1915
|
+
const proceduralEntries = cleanProcedural.map((entry) => this.proceduralToRuntimeEntry(entry));
|
|
1916
|
+
const v2SignalCount = semanticEntries.length + proceduralEntries.length;
|
|
1917
|
+
const useLegacyFallback = v2SignalCount === 0;
|
|
1918
|
+
const byType = (entries, type) => entries.filter((entry) => entry.type === type);
|
|
1919
|
+
const runtimeEntries = [...semanticEntries, ...proceduralEntries];
|
|
1920
|
+
return {
|
|
1921
|
+
knowledge: useLegacyFallback ? legacy.knowledge : byType(runtimeEntries, "fact").filter((entry) => entry.type === "fact"),
|
|
1922
|
+
lessons: useLegacyFallback ? legacy.lessons : byType(runtimeEntries, "lesson"),
|
|
1923
|
+
events: useLegacyFallback ? legacy.events : byType(runtimeEntries, "event"),
|
|
1924
|
+
preferences: useLegacyFallback ? legacy.preferences : byType(runtimeEntries, "preference"),
|
|
1925
|
+
facets: legacy.facets,
|
|
1926
|
+
work,
|
|
1927
|
+
episodes,
|
|
1928
|
+
meta,
|
|
1929
|
+
v2SearchEntries: runtimeEntries,
|
|
1930
|
+
};
|
|
1931
|
+
}
|
|
1932
|
+
getWorkArchiveReason(entry) {
|
|
1933
|
+
if (entry.archivedAt)
|
|
1934
|
+
return entry.archiveReason ?? "pre-archived";
|
|
1935
|
+
if (entry.revivedAt && daysSince(entry.revivedAt) <= this.cfg.forgetting.reviveCooldownDays)
|
|
1936
|
+
return undefined;
|
|
1937
|
+
const anchor = entry.lastAccessed ?? entry.eventTime ?? entry.created;
|
|
1938
|
+
const ageDays = daysSince(anchor);
|
|
1939
|
+
if ((entry.accessCount ?? 0) <= 1 && entry.importance <= 6 && ageDays > this.cfg.forgetting.workTtlDays) {
|
|
1940
|
+
return "stale-work-memory";
|
|
1941
|
+
}
|
|
1942
|
+
return undefined;
|
|
1943
|
+
}
|
|
1944
|
+
getSemanticArchiveReason(entry) {
|
|
1945
|
+
if (entry.archivedAt)
|
|
1946
|
+
return entry.archiveReason ?? "pre-archived";
|
|
1947
|
+
if (entry.revivedAt && daysSince(entry.revivedAt) <= this.cfg.forgetting.reviveCooldownDays)
|
|
1948
|
+
return undefined;
|
|
1949
|
+
const anchor = entry.lastAccessedAt ?? entry.updatedAt ?? entry.createdAt;
|
|
1950
|
+
const ageDays = daysSince(anchor);
|
|
1951
|
+
if (entry.supersededById && ageDays > this.cfg.forgetting.ambientTtlDays) {
|
|
1952
|
+
return "superseded-semantic-memory";
|
|
1953
|
+
}
|
|
1954
|
+
const lowSignal = (entry.accessCount ?? 0) <= 1 && entry.importance <= 6 && entry.confidence < 0.85;
|
|
1955
|
+
if (entry.retention === "ambient" && lowSignal && ageDays > this.cfg.forgetting.ambientTtlDays * 2) {
|
|
1956
|
+
return "stale-semantic-memory";
|
|
1957
|
+
}
|
|
1958
|
+
return undefined;
|
|
1959
|
+
}
|
|
1960
|
+
getProceduralArchiveReason(entry) {
|
|
1961
|
+
if (entry.archivedAt)
|
|
1962
|
+
return entry.archiveReason ?? "pre-archived";
|
|
1963
|
+
if (entry.revivedAt && daysSince(entry.revivedAt) <= this.cfg.forgetting.reviveCooldownDays)
|
|
1964
|
+
return undefined;
|
|
1965
|
+
const anchor = entry.lastAccessedAt ?? entry.updatedAt ?? entry.createdAt;
|
|
1966
|
+
const ageDays = daysSince(anchor);
|
|
1967
|
+
if ((entry.status === "superseded" || entry.status === "deprecated") && ageDays > this.cfg.forgetting.ambientTtlDays) {
|
|
1968
|
+
return "stale-procedure-version";
|
|
1969
|
+
}
|
|
1970
|
+
if (entry.status === "draft" && (entry.accessCount ?? 0) <= 1 && entry.importance <= 6 && ageDays > this.cfg.forgetting.ambientTtlDays * 2) {
|
|
1971
|
+
return "abandoned-draft-procedure";
|
|
1972
|
+
}
|
|
1973
|
+
return undefined;
|
|
1974
|
+
}
|
|
1975
|
+
buildProceduralChains(entries) {
|
|
1976
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
1977
|
+
const roots = entries.filter((entry) => !entry.supersededById || !byId.has(entry.supersededById));
|
|
1978
|
+
return roots
|
|
1979
|
+
.map((root) => {
|
|
1980
|
+
const ids = new Set();
|
|
1981
|
+
const stack = [root.id];
|
|
1982
|
+
while (stack.length) {
|
|
1983
|
+
const currentId = stack.pop();
|
|
1984
|
+
if (!currentId || ids.has(currentId))
|
|
1985
|
+
continue;
|
|
1986
|
+
ids.add(currentId);
|
|
1987
|
+
for (const entry of entries) {
|
|
1988
|
+
if (entry.supersededById === currentId)
|
|
1989
|
+
stack.push(entry.id);
|
|
1990
|
+
if (entry.supersedesIds?.includes(currentId))
|
|
1991
|
+
stack.push(entry.id);
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
const chainEntries = [...ids].map((id) => byId.get(id)).filter((entry) => Boolean(entry));
|
|
1995
|
+
return {
|
|
1996
|
+
rootId: root.id,
|
|
1997
|
+
name: root.name,
|
|
1998
|
+
status: root.status,
|
|
1999
|
+
versionDepth: chainEntries.length,
|
|
2000
|
+
ids: chainEntries
|
|
2001
|
+
.sort((a, b) => {
|
|
2002
|
+
const aVersion = a.version ?? 0;
|
|
2003
|
+
const bVersion = b.version ?? 0;
|
|
2004
|
+
if (aVersion !== bVersion)
|
|
2005
|
+
return bVersion - aVersion;
|
|
2006
|
+
return a.updatedAt.localeCompare(b.updatedAt);
|
|
2007
|
+
})
|
|
2008
|
+
.map((entry) => entry.id),
|
|
2009
|
+
};
|
|
2010
|
+
})
|
|
2011
|
+
.filter((chain) => chain.versionDepth > 1)
|
|
2012
|
+
.sort((a, b) => b.versionDepth - a.versionDepth || a.name.localeCompare(b.name));
|
|
2013
|
+
}
|
|
2014
|
+
materializeV2Links(semantic, procedural, existingLinks, now = new Date().toISOString()) {
|
|
2015
|
+
const manualLinks = existingLinks.filter((link) => !link.id.startsWith(NanoMemEngine.AUTO_V2_LINK_PREFIX));
|
|
2016
|
+
const autoLinks = new Map();
|
|
2017
|
+
const proceduralConflicts = this.detectProceduralConflicts(procedural.filter((entry) => entry.status !== "deprecated" && entry.status !== "superseded"));
|
|
2018
|
+
const semanticConflicts = this.detectSemanticConflicts(semantic);
|
|
2019
|
+
const addAutoLink = (fromId, toId, type, weight, evidence = []) => {
|
|
2020
|
+
if (!fromId || !toId || fromId === toId)
|
|
2021
|
+
return;
|
|
2022
|
+
const directional = type === "supersedes";
|
|
2023
|
+
const [normalizedFrom, normalizedTo] = directional || fromId < toId ? [fromId, toId] : [toId, fromId];
|
|
2024
|
+
const id = `${NanoMemEngine.AUTO_V2_LINK_PREFIX}${type}:${normalizedFrom}->${normalizedTo}`;
|
|
2025
|
+
autoLinks.set(id, {
|
|
2026
|
+
id,
|
|
2027
|
+
fromId: normalizedFrom,
|
|
2028
|
+
toId: normalizedTo,
|
|
2029
|
+
type,
|
|
2030
|
+
weight,
|
|
2031
|
+
explicit: false,
|
|
2032
|
+
createdAt: autoLinks.get(id)?.createdAt ?? now,
|
|
2033
|
+
updatedAt: now,
|
|
2034
|
+
evidence,
|
|
2035
|
+
});
|
|
2036
|
+
};
|
|
2037
|
+
for (const entry of semantic) {
|
|
2038
|
+
for (const supersededId of entry.supersedesIds ?? []) {
|
|
2039
|
+
addAutoLink(entry.id, supersededId, "supersedes", 0.92);
|
|
2040
|
+
}
|
|
2041
|
+
if (entry.supersededById) {
|
|
2042
|
+
addAutoLink(entry.supersededById, entry.id, "supersedes", 0.92);
|
|
2043
|
+
}
|
|
2044
|
+
}
|
|
2045
|
+
for (const entry of procedural) {
|
|
2046
|
+
for (const supersededId of entry.supersedesIds ?? []) {
|
|
2047
|
+
addAutoLink(entry.id, supersededId, "supersedes", 0.94);
|
|
2048
|
+
}
|
|
2049
|
+
if (entry.supersededById) {
|
|
2050
|
+
addAutoLink(entry.supersededById, entry.id, "supersedes", 0.94);
|
|
2051
|
+
}
|
|
2052
|
+
}
|
|
2053
|
+
for (const conflict of semanticConflicts) {
|
|
2054
|
+
addAutoLink(conflict.aId, conflict.bId, "conflicts-with", 0.88);
|
|
2055
|
+
}
|
|
2056
|
+
for (const conflict of proceduralConflicts) {
|
|
2057
|
+
addAutoLink(conflict.aId, conflict.bId, "conflicts-with", Math.max(0.72, conflict.score));
|
|
2058
|
+
}
|
|
2059
|
+
return [...manualLinks, ...autoLinks.values()].sort((a, b) => a.id.localeCompare(b.id));
|
|
2060
|
+
}
|
|
2061
|
+
detectProceduralConflicts(entries) {
|
|
2062
|
+
const conflicts = [];
|
|
2063
|
+
for (let i = 0; i < entries.length; i++) {
|
|
2064
|
+
for (let j = i + 1; j < entries.length; j++) {
|
|
2065
|
+
const a = entries[i];
|
|
2066
|
+
const b = entries[j];
|
|
2067
|
+
if (!a || !b || a.id === b.id)
|
|
2068
|
+
continue;
|
|
2069
|
+
const tagScore = tagOverlap(a.tags ?? [], b.tags ?? []);
|
|
2070
|
+
const lexicalScore = tagOverlap(extractTags(`${a.name} ${a.searchText} ${a.summary}`), extractTags(`${b.name} ${b.searchText} ${b.summary}`));
|
|
2071
|
+
const similarity = Math.max(tagScore, lexicalScore);
|
|
2072
|
+
const sameIntent = a.searchText.trim().toLowerCase() === b.searchText.trim().toLowerCase() ||
|
|
2073
|
+
a.name.trim().toLowerCase() === b.name.trim().toLowerCase();
|
|
2074
|
+
const boundariesDiffer = (a.boundaries ?? "").trim().toLowerCase() !== (b.boundaries ?? "").trim().toLowerCase() ||
|
|
2075
|
+
(a.contextText ?? "").trim().toLowerCase() !== (b.contextText ?? "").trim().toLowerCase();
|
|
2076
|
+
if ((similarity >= 0.72 || sameIntent) && boundariesDiffer) {
|
|
2077
|
+
conflicts.push({
|
|
2078
|
+
aId: a.id,
|
|
2079
|
+
bId: b.id,
|
|
2080
|
+
aName: a.name,
|
|
2081
|
+
bName: b.name,
|
|
2082
|
+
score: Number(similarity.toFixed(3)),
|
|
2083
|
+
reason: "High-overlap active procedures differ in boundaries or context.",
|
|
2084
|
+
});
|
|
2085
|
+
}
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
return conflicts.sort((a, b) => b.score - a.score || a.aName.localeCompare(b.aName));
|
|
2089
|
+
}
|
|
2090
|
+
detectSemanticConflicts(entries) {
|
|
2091
|
+
const byId = new Map(entries.map((entry) => [entry.id, entry]));
|
|
2092
|
+
const seen = new Set();
|
|
2093
|
+
const conflicts = [];
|
|
2094
|
+
for (const entry of entries) {
|
|
2095
|
+
for (const conflictId of entry.conflictWithIds ?? []) {
|
|
2096
|
+
const other = byId.get(conflictId);
|
|
2097
|
+
if (!other)
|
|
2098
|
+
continue;
|
|
2099
|
+
const pairKey = [entry.id, other.id].sort().join("::");
|
|
2100
|
+
if (seen.has(pairKey))
|
|
2101
|
+
continue;
|
|
2102
|
+
seen.add(pairKey);
|
|
2103
|
+
conflicts.push({
|
|
2104
|
+
aId: entry.id,
|
|
2105
|
+
bId: other.id,
|
|
2106
|
+
aName: entry.name,
|
|
2107
|
+
bName: other.name,
|
|
2108
|
+
reason: "Explicit semantic conflict link.",
|
|
2109
|
+
});
|
|
2110
|
+
}
|
|
2111
|
+
}
|
|
2112
|
+
return conflicts.sort((a, b) => a.aName.localeCompare(b.aName) || a.bName.localeCompare(b.bName));
|
|
2113
|
+
}
|
|
1271
2114
|
buildEmbeddingSourceItems(episodes, facets, semantic, procedural) {
|
|
1272
2115
|
const episodeItems = episodes.map((entry) => ({
|
|
1273
2116
|
memoryId: entry.id,
|
|
@@ -1437,6 +2280,7 @@ export class NanoMemEngine {
|
|
|
1437
2280
|
lastReconsolidationAt: new Date().toISOString(),
|
|
1438
2281
|
}),
|
|
1439
2282
|
]);
|
|
2283
|
+
await this.rebuildV2Links();
|
|
1440
2284
|
}
|
|
1441
2285
|
detectAlignmentConflicts(entries) {
|
|
1442
2286
|
const candidates = entries.filter((entry) => entry.stability !== "situational" &&
|