@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.
@@ -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 = [...activeKnowledge, ...dedupedCueKnowledge];
420
- const allRecalledLessons = [...activeLessons, ...dedupedCueLessons];
421
- const allRecalledEvents = [...activeEvents, ...dedupedCueEvents];
422
- const allRecalledPrefs = [...activePrefs, ...dedupedCuePrefs];
423
- const allRecalledFacets = [...activeFacets, ...dedupedCueFacets];
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: activeKnowledge,
442
- lessons: activeLessons,
443
- events: activeEvents,
444
- preferences: activePrefs,
445
- facets: activeFacets,
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: dedupedCueKnowledge,
452
- lessons: dedupedCueLessons,
453
- events: dedupedCueEvents,
454
- preferences: dedupedCuePrefs,
455
- facets: dedupedCueFacets,
456
- episodes: topEpisodes,
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 = 1) {
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 { knowledge, lessons, events, preferences, facets } = await this.getAllEntries();
959
- const all = [...knowledge, ...lessons, ...events, ...preferences, ...facets];
960
- return filterByScope(all, scope)
961
- .map((e) => ({
962
- entry: e,
963
- score: tagOverlap(e.tags, tags) +
964
- (e.relations ?? [])
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((x) => x.score > 0)
1055
+ .filter((item) => item.score > 0)
969
1056
  .sort((a, b) => b.score - a.score)
970
- .map((x) => x.entry);
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.exportAll();
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.exportAll();
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.exportAll();
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.exportAll();
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 true;
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 true;
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" &&