anno-extension 0.1.0

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.
Files changed (199) hide show
  1. package/LICENSE +7 -0
  2. package/README.md +30 -0
  3. package/THIRD_PARTY_NOTICES.md +8 -0
  4. package/dist/browser-fs.d.ts +14 -0
  5. package/dist/browser-fs.d.ts.map +1 -0
  6. package/dist/browser-fs.js +69 -0
  7. package/dist/browser-fs.js.map +1 -0
  8. package/dist/config.d.ts +81 -0
  9. package/dist/config.d.ts.map +1 -0
  10. package/dist/config.js +82 -0
  11. package/dist/config.js.map +1 -0
  12. package/dist/consolidate-v2.d.ts +15 -0
  13. package/dist/consolidate-v2.d.ts.map +1 -0
  14. package/dist/consolidate-v2.js +125 -0
  15. package/dist/consolidate-v2.js.map +1 -0
  16. package/dist/consolidation.d.ts +12 -0
  17. package/dist/consolidation.d.ts.map +1 -0
  18. package/dist/consolidation.js +199 -0
  19. package/dist/consolidation.js.map +1 -0
  20. package/dist/dedup.d.ts +18 -0
  21. package/dist/dedup.d.ts.map +1 -0
  22. package/dist/dedup.js +85 -0
  23. package/dist/dedup.js.map +1 -0
  24. package/dist/diagnostics.d.ts +24 -0
  25. package/dist/diagnostics.d.ts.map +1 -0
  26. package/dist/diagnostics.js +53 -0
  27. package/dist/diagnostics.js.map +1 -0
  28. package/dist/embedding-index.d.ts +27 -0
  29. package/dist/embedding-index.d.ts.map +1 -0
  30. package/dist/embedding-index.js +111 -0
  31. package/dist/embedding-index.js.map +1 -0
  32. package/dist/engine-archive.d.ts +37 -0
  33. package/dist/engine-archive.d.ts.map +1 -0
  34. package/dist/engine-archive.js +155 -0
  35. package/dist/engine-archive.js.map +1 -0
  36. package/dist/engine-episode-sync.d.ts +19 -0
  37. package/dist/engine-episode-sync.d.ts.map +1 -0
  38. package/dist/engine-episode-sync.js +186 -0
  39. package/dist/engine-episode-sync.js.map +1 -0
  40. package/dist/engine-injection-text.d.ts +46 -0
  41. package/dist/engine-injection-text.d.ts.map +1 -0
  42. package/dist/engine-injection-text.js +271 -0
  43. package/dist/engine-injection-text.js.map +1 -0
  44. package/dist/engine-insights.d.ts +22 -0
  45. package/dist/engine-insights.d.ts.map +1 -0
  46. package/dist/engine-insights.js +127 -0
  47. package/dist/engine-insights.js.map +1 -0
  48. package/dist/engine-links.d.ts +43 -0
  49. package/dist/engine-links.d.ts.map +1 -0
  50. package/dist/engine-links.js +223 -0
  51. package/dist/engine-links.js.map +1 -0
  52. package/dist/engine-recall-select.d.ts +84 -0
  53. package/dist/engine-recall-select.d.ts.map +1 -0
  54. package/dist/engine-recall-select.js +127 -0
  55. package/dist/engine-recall-select.js.map +1 -0
  56. package/dist/engine-reinforce.d.ts +18 -0
  57. package/dist/engine-reinforce.d.ts.map +1 -0
  58. package/dist/engine-reinforce.js +116 -0
  59. package/dist/engine-reinforce.js.map +1 -0
  60. package/dist/engine-scoring-v2.d.ts +35 -0
  61. package/dist/engine-scoring-v2.d.ts.map +1 -0
  62. package/dist/engine-scoring-v2.js +178 -0
  63. package/dist/engine-scoring-v2.js.map +1 -0
  64. package/dist/engine-v2-mapping.d.ts +17 -0
  65. package/dist/engine-v2-mapping.d.ts.map +1 -0
  66. package/dist/engine-v2-mapping.js +190 -0
  67. package/dist/engine-v2-mapping.js.map +1 -0
  68. package/dist/engine.d.ts +339 -0
  69. package/dist/engine.d.ts.map +1 -0
  70. package/dist/engine.js +1964 -0
  71. package/dist/engine.js.map +1 -0
  72. package/dist/eviction.d.ts +14 -0
  73. package/dist/eviction.d.ts.map +1 -0
  74. package/dist/eviction.js +26 -0
  75. package/dist/eviction.js.map +1 -0
  76. package/dist/extraction.d.ts +11 -0
  77. package/dist/extraction.d.ts.map +1 -0
  78. package/dist/extraction.js +261 -0
  79. package/dist/extraction.js.map +1 -0
  80. package/dist/full-insights-html.d.ts +9 -0
  81. package/dist/full-insights-html.d.ts.map +1 -0
  82. package/dist/full-insights-html.js +422 -0
  83. package/dist/full-insights-html.js.map +1 -0
  84. package/dist/full-insights-sections.d.ts +48 -0
  85. package/dist/full-insights-sections.d.ts.map +1 -0
  86. package/dist/full-insights-sections.js +231 -0
  87. package/dist/full-insights-sections.js.map +1 -0
  88. package/dist/full-insights.d.ts +22 -0
  89. package/dist/full-insights.d.ts.map +1 -0
  90. package/dist/full-insights.js +370 -0
  91. package/dist/full-insights.js.map +1 -0
  92. package/dist/hash-embedding.d.ts +9 -0
  93. package/dist/hash-embedding.d.ts.map +1 -0
  94. package/dist/hash-embedding.js +44 -0
  95. package/dist/hash-embedding.js.map +1 -0
  96. package/dist/human-insights.d.ts +28 -0
  97. package/dist/human-insights.d.ts.map +1 -0
  98. package/dist/human-insights.js +260 -0
  99. package/dist/human-insights.js.map +1 -0
  100. package/dist/i18n.d.ts +64 -0
  101. package/dist/i18n.d.ts.map +1 -0
  102. package/dist/i18n.js +367 -0
  103. package/dist/i18n.js.map +1 -0
  104. package/dist/index.d.ts +27 -0
  105. package/dist/index.d.ts.map +1 -0
  106. package/dist/index.js +21 -0
  107. package/dist/index.js.map +1 -0
  108. package/dist/insights-html.d.ts +9 -0
  109. package/dist/insights-html.d.ts.map +1 -0
  110. package/dist/insights-html.js +432 -0
  111. package/dist/insights-html.js.map +1 -0
  112. package/dist/linking.d.ts +22 -0
  113. package/dist/linking.d.ts.map +1 -0
  114. package/dist/linking.js +192 -0
  115. package/dist/linking.js.map +1 -0
  116. package/dist/llm-json.d.ts +10 -0
  117. package/dist/llm-json.d.ts.map +1 -0
  118. package/dist/llm-json.js +193 -0
  119. package/dist/llm-json.js.map +1 -0
  120. package/dist/privacy.d.ts +17 -0
  121. package/dist/privacy.d.ts.map +1 -0
  122. package/dist/privacy.js +53 -0
  123. package/dist/privacy.js.map +1 -0
  124. package/dist/procedural-v2.d.ts +9 -0
  125. package/dist/procedural-v2.d.ts.map +1 -0
  126. package/dist/procedural-v2.js +123 -0
  127. package/dist/procedural-v2.js.map +1 -0
  128. package/dist/reconsolidate-v2.d.ts +19 -0
  129. package/dist/reconsolidate-v2.d.ts.map +1 -0
  130. package/dist/reconsolidate-v2.js +115 -0
  131. package/dist/reconsolidate-v2.js.map +1 -0
  132. package/dist/scoring.d.ts +32 -0
  133. package/dist/scoring.d.ts.map +1 -0
  134. package/dist/scoring.js +108 -0
  135. package/dist/scoring.js.map +1 -0
  136. package/dist/store-v2.d.ts +35 -0
  137. package/dist/store-v2.d.ts.map +1 -0
  138. package/dist/store-v2.js +98 -0
  139. package/dist/store-v2.js.map +1 -0
  140. package/dist/store.d.ts +22 -0
  141. package/dist/store.d.ts.map +1 -0
  142. package/dist/store.js +116 -0
  143. package/dist/store.js.map +1 -0
  144. package/dist/turn-context.d.ts +37 -0
  145. package/dist/turn-context.d.ts.map +1 -0
  146. package/dist/turn-context.js +31 -0
  147. package/dist/turn-context.js.map +1 -0
  148. package/dist/types-v2.d.ts +190 -0
  149. package/dist/types-v2.d.ts.map +1 -0
  150. package/dist/types-v2.js +8 -0
  151. package/dist/types-v2.js.map +1 -0
  152. package/dist/types.d.ts +330 -0
  153. package/dist/types.d.ts.map +1 -0
  154. package/dist/types.js +8 -0
  155. package/dist/types.js.map +1 -0
  156. package/dist/update.d.ts +31 -0
  157. package/dist/update.d.ts.map +1 -0
  158. package/dist/update.js +250 -0
  159. package/dist/update.js.map +1 -0
  160. package/package.json +34 -0
  161. package/src/browser-fs.ts +67 -0
  162. package/src/config.ts +138 -0
  163. package/src/consolidate-v2.ts +158 -0
  164. package/src/consolidation.ts +229 -0
  165. package/src/dedup.ts +91 -0
  166. package/src/diagnostics.ts +81 -0
  167. package/src/embedding-index.ts +138 -0
  168. package/src/engine-archive.ts +167 -0
  169. package/src/engine-episode-sync.ts +237 -0
  170. package/src/engine-injection-text.ts +334 -0
  171. package/src/engine-insights.ts +182 -0
  172. package/src/engine-links.ts +311 -0
  173. package/src/engine-recall-select.ts +298 -0
  174. package/src/engine-reinforce.ts +154 -0
  175. package/src/engine-scoring-v2.ts +188 -0
  176. package/src/engine-v2-mapping.ts +204 -0
  177. package/src/engine.ts +2499 -0
  178. package/src/eviction.ts +38 -0
  179. package/src/extraction.ts +300 -0
  180. package/src/full-insights-html.ts +474 -0
  181. package/src/full-insights-sections.ts +305 -0
  182. package/src/full-insights.ts +423 -0
  183. package/src/hash-embedding.ts +48 -0
  184. package/src/human-insights.ts +345 -0
  185. package/src/i18n.ts +455 -0
  186. package/src/index.ts +117 -0
  187. package/src/insights-html.ts +478 -0
  188. package/src/linking.ts +228 -0
  189. package/src/llm-json.ts +194 -0
  190. package/src/privacy.ts +54 -0
  191. package/src/procedural-v2.ts +142 -0
  192. package/src/reconsolidate-v2.ts +145 -0
  193. package/src/scoring.ts +156 -0
  194. package/src/store-v2.ts +138 -0
  195. package/src/store.ts +137 -0
  196. package/src/turn-context.ts +56 -0
  197. package/src/types-v2.ts +244 -0
  198. package/src/types.ts +372 -0
  199. package/src/update.ts +271 -0
package/dist/engine.js ADDED
@@ -0,0 +1,1964 @@
1
+ /**
2
+ * [WHO]: NanoMemEngine class - unified facade for memory CRUD, injection, consolidation; delegates to engine-* modules
3
+ * [FROM]: Uses browser-fs.ts and the NanoMem core modules below
4
+ * [TO]: Consumed by packages/mem-core/src/index.ts, extension.ts, cli.ts, test/*
5
+ * [HERE]: packages/mem-core/src/engine.ts - facade layer composing all memory subsystems via thin delegation wrappers
6
+ */
7
+ import { cp, mkdir, readdir, join } from "./browser-fs.js";
8
+ import { getConfig } from "./config.js";
9
+ import { consolidateEpisodes } from "./consolidation.js";
10
+ import { consolidateV2Memories } from "./consolidate-v2.js";
11
+ import { queryEmbeddingIndex, syncEmbeddingIndex, loadEmbeddingIndex, saveEmbeddingIndex, } from "./embedding-index.js";
12
+ import { utilityEntry, utilityWork } from "./eviction.js";
13
+ import { extractMemories, extractWork } from "./extraction.js";
14
+ import { createHashedEmbeddingFn } from "./hash-embedding.js";
15
+ import { PROMPTS } from "./i18n.js";
16
+ import { linkNewEntry, reinforceRelations, } from "./linking.js";
17
+ import { evictExpiredEntries, evictExpiredWork, filterByScope, filterPII } from "./privacy.js";
18
+ import { daysSince, extractTags, scoreEntry, tagOverlap } from "./scoring.js";
19
+ import { loadEntries, loadEpisodes, loadMeta, loadWork, saveEpisode as persistEpisode, saveEntries, saveWork, writeJson, } from "./store.js";
20
+ import { deduplicateMemoryEntries, deduplicateWorkEntries } from "./dedup.js";
21
+ import { buildFullInsightsReport } from "./full-insights.js";
22
+ import { generateHumanInsights } from "./human-insights.js";
23
+ import { reconsolidateV2Memories } from "./reconsolidate-v2.js";
24
+ import { getV2Paths, loadV2Episodes, loadV2Facets, loadV2Links, loadV2Meta, loadV2Procedural, loadV2Semantic, loadV2State, saveV2Episodes, saveV2Facets, saveV2Links, saveV2Meta, saveV2Procedural, saveV2Semantic, } from "./store-v2.js";
25
+ import { compileProcedureFromEpisode } from "./procedural-v2.js";
26
+ import { applyExtraction, checkConsolidationEntry, checkWorkDuplicate } from "./update.js";
27
+ import { computeStructuralBoost, currentStructuralAnchor, scoreEpisodeFacet as scoreEpisodeFacetFn, scoreEpisodeMemory as scoreEpisodeMemoryFn, scoreProceduralMemory as scoreProceduralMemoryFn, scoreV2SemanticMemory as scoreV2SemanticMemoryFn, breakdownEpisodeMemory, breakdownEpisodeFacet, breakdownV2Semantic, breakdownProcedural, } from "./engine-scoring-v2.js";
28
+ import { CONVERSATION_PREFERENCE_PATTERNS, buildInjectedMemoryOrder, buildProgressiveInjectionText, isConversationPreference as isConversationPreferenceFn, selectConversationPreferences as selectConversationPreferencesFn, rankConversationPreference as rankConversationPreferenceFn, } from "./engine-injection-text.js";
29
+ import { inferSemanticRetention as inferSemanticRetentionFn, inferSemanticStability as inferSemanticStabilityFn, inferSemanticImportance as inferSemanticImportanceFn, mapExtractedItemToSemanticType as mapExtractedItemToSemanticTypeFn, upsertSemanticFromExtractedItem as upsertSemanticFromExtractedItemFn, semanticKindToLegacyType as semanticKindToLegacyTypeFn, semanticToRuntimeEntry as semanticToRuntimeEntryFn, proceduralToRuntimeEntry as proceduralToRuntimeEntryFn, } from "./engine-v2-mapping.js";
30
+ import { mergeArchivedEntries as mergeArchivedEntriesFn, mergeArchivedWork as mergeArchivedWorkFn, mergeArchivedV2 as mergeArchivedV2Fn, partitionArchivedEntries as partitionArchivedEntriesFn, partitionArchivedWork as partitionArchivedWorkFn, partitionArchivedSemantic as partitionArchivedSemanticFn, partitionArchivedProcedural as partitionArchivedProceduralFn, } from "./engine-archive.js";
31
+ import { AUTO_V2_LINK_PREFIX, buildProceduralChains as buildProceduralChainsFn, materializeV2Links as materializeV2LinksFn, detectProceduralConflicts as detectProceduralConflictsFn, detectSemanticConflicts as detectSemanticConflictsFn, detectAlignmentConflicts as detectAlignmentConflictsFn, suggestConflictAction as suggestConflictActionFn, } from "./engine-links.js";
32
+ import { generateInsightsReport } from "./engine-insights.js";
33
+ import { selectRecallEntries } from "./engine-recall-select.js";
34
+ import { setTurnContext } from "./turn-context.js";
35
+ export class NanoMemEngine {
36
+ cfg;
37
+ llmFn;
38
+ embeddingFn;
39
+ static AUTO_V2_LINK_PREFIX = AUTO_V2_LINK_PREFIX;
40
+ static AUTO_REVIVE_MAX_ITEMS = 2;
41
+ static CONVERSATION_PREFERENCE_PATTERNS = CONVERSATION_PREFERENCE_PATTERNS;
42
+ knowledgePath;
43
+ lessonsPath;
44
+ eventsPath;
45
+ preferencesPath;
46
+ facetsPath;
47
+ workPath;
48
+ metaPath;
49
+ episodesDir;
50
+ v2Paths;
51
+ archiveKnowledgePath;
52
+ archiveLessonsPath;
53
+ archiveEventsPath;
54
+ archivePreferencesPath;
55
+ archiveFacetsPath;
56
+ archiveWorkPath;
57
+ archiveV2Paths;
58
+ constructor(overrides, llmFn) {
59
+ this.cfg = getConfig(overrides);
60
+ this.llmFn = llmFn;
61
+ if (this.cfg.embeddings.enabled) {
62
+ this.embeddingFn = createHashedEmbeddingFn(this.cfg.embeddings.dim);
63
+ }
64
+ this.knowledgePath = join(this.cfg.memoryDir, "knowledge.json");
65
+ this.lessonsPath = join(this.cfg.memoryDir, "lessons.json");
66
+ this.eventsPath = join(this.cfg.memoryDir, "events.json");
67
+ this.preferencesPath = join(this.cfg.memoryDir, "preferences.json");
68
+ this.facetsPath = join(this.cfg.memoryDir, "facets.json");
69
+ this.workPath = join(this.cfg.memoryDir, "work.json");
70
+ this.metaPath = join(this.cfg.memoryDir, "meta.json");
71
+ this.episodesDir = join(this.cfg.memoryDir, "episodes");
72
+ this.v2Paths = getV2Paths(this.cfg.memoryDir);
73
+ const archiveDir = join(this.cfg.memoryDir, "_archive");
74
+ this.archiveKnowledgePath = join(archiveDir, "knowledge.json");
75
+ this.archiveLessonsPath = join(archiveDir, "lessons.json");
76
+ this.archiveEventsPath = join(archiveDir, "events.json");
77
+ this.archivePreferencesPath = join(archiveDir, "preferences.json");
78
+ this.archiveFacetsPath = join(archiveDir, "facets.json");
79
+ this.archiveWorkPath = join(archiveDir, "work.json");
80
+ this.archiveV2Paths = getV2Paths(archiveDir);
81
+ }
82
+ setLlmFn(fn) {
83
+ this.llmFn = fn;
84
+ }
85
+ setEmbeddingFn(fn) {
86
+ this.embeddingFn = fn;
87
+ }
88
+ // ─── Extraction ──────────────────────────────────────────────
89
+ async extractAndStore(conversation, project) {
90
+ const items = await extractMemories(conversation, this.cfg, this.llmFn);
91
+ if (!items.length)
92
+ return [];
93
+ const knowledge = await loadEntries(this.knowledgePath);
94
+ const lessons = await loadEntries(this.lessonsPath);
95
+ const events = await loadEntries(this.eventsPath);
96
+ const prefs = await loadEntries(this.preferencesPath);
97
+ const facets = await loadEntries(this.facetsPath);
98
+ const v2Semantic = await loadV2Semantic(this.v2Paths);
99
+ for (const item of items) {
100
+ const target = item.type === "lesson"
101
+ ? lessons
102
+ : item.type === "event"
103
+ ? events
104
+ : item.type === "preference"
105
+ ? prefs
106
+ : item.type === "pattern" || item.type === "struggle"
107
+ ? facets
108
+ : knowledge;
109
+ applyExtraction(target, item, project, this.cfg);
110
+ this.upsertSemanticFromExtractedItem(v2Semantic, item, project);
111
+ const stored = target.find(entry => entry.name === item.name && entry.summary === item.summary);
112
+ const mirror = v2Semantic.find(entry => entry.name === item.name && entry.summary === item.summary && entry.scope?.project === project);
113
+ if (stored && mirror)
114
+ mirror.legacyIds = [...new Set([...(mirror.legacyIds ?? []), stored.id])];
115
+ }
116
+ const hl = this.cfg.halfLife;
117
+ const ew = this.cfg.evictionWeights;
118
+ await Promise.all([
119
+ saveEntries(this.knowledgePath, knowledge, this.cfg.maxEntries.knowledge, (e) => utilityEntry(e, hl, ew)),
120
+ saveEntries(this.lessonsPath, lessons, this.cfg.maxEntries.lessons, (e) => utilityEntry(e, hl, ew)),
121
+ saveEntries(this.eventsPath, events, this.cfg.maxEntries.events, (e) => utilityEntry(e, hl, ew)),
122
+ saveEntries(this.preferencesPath, prefs, this.cfg.maxEntries.preferences, (e) => utilityEntry(e, hl, ew)),
123
+ saveEntries(this.facetsPath, facets, this.cfg.maxEntries.facets, (e) => utilityEntry(e, hl, ew)),
124
+ saveV2Semantic(this.v2Paths, v2Semantic),
125
+ ]);
126
+ return items;
127
+ }
128
+ async remember(item, project) {
129
+ const knowledge = await loadEntries(this.knowledgePath);
130
+ const lessons = await loadEntries(this.lessonsPath);
131
+ const events = await loadEntries(this.eventsPath);
132
+ const prefs = await loadEntries(this.preferencesPath);
133
+ const facets = await loadEntries(this.facetsPath);
134
+ const v2Semantic = await loadV2Semantic(this.v2Paths);
135
+ let target = knowledge;
136
+ if (item.type === "lesson")
137
+ target = lessons;
138
+ if (item.type === "event")
139
+ target = events;
140
+ if (item.type === "preference")
141
+ target = prefs;
142
+ if (item.type === "pattern" || item.type === "struggle")
143
+ target = facets;
144
+ applyExtraction(target, item, project, this.cfg);
145
+ this.upsertSemanticFromExtractedItem(v2Semantic, item, project);
146
+ const stored = target.find(entry => entry.name === item.name && entry.summary === item.summary);
147
+ const mirror = v2Semantic.find(entry => entry.name === item.name && entry.summary === item.summary && entry.scope?.project === project);
148
+ if (stored && mirror)
149
+ mirror.legacyIds = [...new Set([...(mirror.legacyIds ?? []), stored.id])];
150
+ const hl = this.cfg.halfLife;
151
+ const ew = this.cfg.evictionWeights;
152
+ await Promise.all([
153
+ saveEntries(this.knowledgePath, knowledge, this.cfg.maxEntries.knowledge, (e) => utilityEntry(e, hl, ew)),
154
+ saveEntries(this.lessonsPath, lessons, this.cfg.maxEntries.lessons, (e) => utilityEntry(e, hl, ew)),
155
+ saveEntries(this.eventsPath, events, this.cfg.maxEntries.events, (e) => utilityEntry(e, hl, ew)),
156
+ saveEntries(this.preferencesPath, prefs, this.cfg.maxEntries.preferences, (e) => utilityEntry(e, hl, ew)),
157
+ saveEntries(this.facetsPath, facets, this.cfg.maxEntries.facets, (e) => utilityEntry(e, hl, ew)),
158
+ saveV2Semantic(this.v2Paths, v2Semantic),
159
+ ]);
160
+ return target.find((entry) => entry.name === item.name || entry.summary === item.summary) ?? target.at(-1) ?? null;
161
+ }
162
+ async extractAndStoreWork(conversation, project, sessionGoal) {
163
+ const extracted = await extractWork(conversation, this.cfg, this.llmFn);
164
+ if (!extracted || (!extracted.goal && !extracted.summary))
165
+ return;
166
+ const entries = await loadWork(this.workPath);
167
+ const now = new Date().toISOString();
168
+ const newWork = {
169
+ id: `${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
170
+ goal: sessionGoal || extracted.goal,
171
+ summary: filterPII(extracted.summary),
172
+ detail: extracted.detail ? filterPII(extracted.detail) : undefined,
173
+ project,
174
+ tags: extractTags(`${extracted.goal} ${extracted.summary}`),
175
+ importance: 6,
176
+ strength: this.cfg.halfLife.work ?? 45,
177
+ created: now,
178
+ eventTime: now,
179
+ accessCount: 0,
180
+ relatedIds: [],
181
+ ttl: this.cfg.forgetting.workTtlDays,
182
+ scope: this.cfg.defaultScope,
183
+ };
184
+ const result = checkWorkDuplicate(entries, newWork);
185
+ if (result.action === "skip")
186
+ return;
187
+ if (result.action === "update" && result.index !== undefined) {
188
+ const existing = entries[result.index];
189
+ entries[result.index] = {
190
+ ...existing,
191
+ goal: newWork.goal,
192
+ summary: newWork.summary,
193
+ detail: newWork.detail ?? existing.detail,
194
+ tags: newWork.tags,
195
+ lastAccessed: now,
196
+ };
197
+ }
198
+ else {
199
+ entries.push(newWork);
200
+ }
201
+ await saveWork(this.workPath, entries, this.cfg.maxEntries.work, (w) => utilityWork(w, this.cfg.halfLife, this.cfg.evictionWeights));
202
+ }
203
+ // ─── Episode Management ──────────────────────────────────────
204
+ async saveEpisode(ep) {
205
+ const existing = (await loadEpisodes(this.episodesDir)).find(item => item.sessionId === ep.sessionId);
206
+ if (existing)
207
+ ep = existing;
208
+ else
209
+ await persistEpisode(this.episodesDir, ep);
210
+ const meta = await loadMeta(this.metaPath);
211
+ meta.totalSessions = (await loadEpisodes(this.episodesDir)).length;
212
+ await writeJson(this.metaPath, meta);
213
+ await this.syncEpisodeToV2(ep);
214
+ }
215
+ async getV2EpisodeMemories() {
216
+ return loadV2Episodes(this.v2Paths);
217
+ }
218
+ async getV2Snapshot() {
219
+ const [episodes, facets, semantic, procedural, links] = await Promise.all([
220
+ loadV2Episodes(this.v2Paths),
221
+ loadV2Facets(this.v2Paths),
222
+ loadV2Semantic(this.v2Paths),
223
+ loadV2Procedural(this.v2Paths),
224
+ loadV2Links(this.v2Paths),
225
+ ]);
226
+ return { episodes, facets, semantic, procedural, links };
227
+ }
228
+ async searchV2Memories(query, limit = 10, scope) {
229
+ await this.autoReviveRelevantArchive(query, scope);
230
+ const snapshot = await this.getV2Snapshot();
231
+ const project = scope?.project ?? "";
232
+ const episodes = this.filterAndCleanV2Episodes(snapshot.episodes, project, scope);
233
+ const facets = this.filterAndCleanV2Facets(snapshot.facets, project, scope);
234
+ const semantic = this.filterAndCleanV2Semantic(snapshot.semantic, project, scope);
235
+ const procedural = this.filterAndCleanProcedural(snapshot.procedural, project, scope);
236
+ const matches = await this.querySemanticCandidates(query, project, scope, episodes, facets, semantic, procedural);
237
+ const episodeById = new Map(episodes.map((entry) => [entry.id, entry]));
238
+ const facetById = new Map(facets.map((entry) => [entry.id, entry]));
239
+ const semanticById = new Map(semantic.map((entry) => [entry.id, entry]));
240
+ const proceduralById = new Map(procedural.map((entry) => [entry.id, entry]));
241
+ const results = [];
242
+ for (const match of matches.slice(0, limit)) {
243
+ if (match.memoryKind === "episode") {
244
+ const entry = episodeById.get(match.memoryId);
245
+ if (entry) {
246
+ results.push({ kind: "episode", id: entry.id, title: entry.title || "Episode", summary: entry.summary, score: match.score });
247
+ }
248
+ continue;
249
+ }
250
+ if (match.memoryKind === "facet") {
251
+ const entry = facetById.get(match.memoryId);
252
+ if (entry) {
253
+ results.push({ kind: "facet", id: entry.id, title: entry.searchText, summary: entry.summary || entry.anchorText || "", score: match.score });
254
+ }
255
+ continue;
256
+ }
257
+ if (match.memoryKind === "semantic") {
258
+ const entry = semanticById.get(match.memoryId);
259
+ if (entry) {
260
+ results.push({ kind: "semantic", id: entry.id, title: entry.name, summary: entry.summary, score: match.score });
261
+ }
262
+ continue;
263
+ }
264
+ const entry = proceduralById.get(match.memoryId);
265
+ if (entry) {
266
+ results.push({ kind: "procedural", id: entry.id, title: entry.name, summary: entry.summary, score: match.score });
267
+ }
268
+ }
269
+ return results;
270
+ }
271
+ async syncV2Embeddings(model = this.cfg.embeddings.model) {
272
+ if (!this.embeddingFn)
273
+ return 0;
274
+ const snapshot = await this.getV2Snapshot();
275
+ const items = this.buildEmbeddingSourceItems(snapshot.episodes, snapshot.facets, snapshot.semantic, snapshot.procedural);
276
+ await syncEmbeddingIndex(this.cfg.memoryDir, model, items, this.embeddingFn);
277
+ const meta = await loadV2Meta(this.v2Paths);
278
+ await saveV2Meta(this.v2Paths, {
279
+ ...meta,
280
+ lastEmbeddingSyncAt: new Date().toISOString(),
281
+ });
282
+ return items.length;
283
+ }
284
+ async consolidate() {
285
+ const result = await this.consolidateDetailed();
286
+ return result.entries;
287
+ }
288
+ async consolidateDetailed(options) {
289
+ const episodes = await loadEpisodes(this.episodesDir);
290
+ const unconsolidatedCount = episodes.filter((ep) => !ep.consolidated).length;
291
+ if (options?.signal?.aborted) {
292
+ throw new Error("AbortError");
293
+ }
294
+ let newEntries = [];
295
+ try {
296
+ newEntries = await consolidateEpisodes(episodes, this.cfg, this.llmFn, options);
297
+ }
298
+ catch (e) {
299
+ if (e instanceof Error && e.message === "AbortError")
300
+ throw e;
301
+ throw e;
302
+ }
303
+ if (!newEntries.length) {
304
+ return { entries: [], stats: { episodesConsidered: unconsolidatedCount, added: 0, updated: 0, skipped: 0 } };
305
+ }
306
+ const knowledge = await loadEntries(this.knowledgePath);
307
+ const lessons = await loadEntries(this.lessonsPath);
308
+ const events = await loadEntries(this.eventsPath);
309
+ const [v2Episodes, v2Facets, v2Procedural, v2Semantic] = await Promise.all([
310
+ loadV2Episodes(this.v2Paths),
311
+ loadV2Facets(this.v2Paths),
312
+ loadV2Procedural(this.v2Paths),
313
+ loadV2Semantic(this.v2Paths),
314
+ ]);
315
+ const v2Consolidated = consolidateV2Memories(v2Episodes, v2Facets, v2Procedural, this.cfg);
316
+ newEntries = [...newEntries, ...v2Consolidated.entries];
317
+ const allExisting = [...knowledge, ...lessons, ...events];
318
+ let added = 0;
319
+ let updated = 0;
320
+ let skipped = 0;
321
+ for (const entry of newEntries) {
322
+ if (options?.signal?.aborted) {
323
+ throw new Error("AbortError");
324
+ }
325
+ const target = entry.type === "lesson" ? lessons : entry.type === "event" ? events : knowledge;
326
+ const result = checkConsolidationEntry(target, entry, allExisting);
327
+ if (result.action === "skip") {
328
+ skipped++;
329
+ continue;
330
+ }
331
+ if (result.action === "update" && result.index !== undefined) {
332
+ updated++;
333
+ const existing = target[result.index];
334
+ target[result.index] = {
335
+ ...existing,
336
+ name: entry.name,
337
+ summary: entry.summary,
338
+ detail: entry.detail,
339
+ content: entry.detail,
340
+ tags: entry.tags,
341
+ lastAccessed: new Date().toISOString(),
342
+ };
343
+ }
344
+ else {
345
+ added++;
346
+ linkNewEntry(entry, allExisting);
347
+ target.push(entry);
348
+ allExisting.push(entry);
349
+ }
350
+ }
351
+ if (options?.signal?.aborted) {
352
+ throw new Error("AbortError");
353
+ }
354
+ const hl = this.cfg.halfLife;
355
+ const ew = this.cfg.evictionWeights;
356
+ await Promise.all([
357
+ saveEntries(this.knowledgePath, knowledge, this.cfg.maxEntries.knowledge, (e) => utilityEntry(e, hl, ew)),
358
+ saveEntries(this.lessonsPath, lessons, this.cfg.maxEntries.lessons, (e) => utilityEntry(e, hl, ew)),
359
+ saveEntries(this.eventsPath, events, this.cfg.maxEntries.events, (e) => utilityEntry(e, hl, ew)),
360
+ ]);
361
+ const nextSemantic = [...v2Semantic];
362
+ for (const item of v2Consolidated.semantic) {
363
+ if (!nextSemantic.some((existing) => tagOverlap(existing.tags, item.tags) >= 0.8 && existing.semanticType === item.semanticType)) {
364
+ nextSemantic.push(item);
365
+ }
366
+ }
367
+ for (const episode of v2Episodes) {
368
+ const derived = v2Consolidated.episodeSemanticMap.get(episode.id);
369
+ if (!derived?.length)
370
+ continue;
371
+ episode.derivedSemanticIds = [...new Set([...(episode.derivedSemanticIds ?? []), ...derived])];
372
+ episode.consolidatedAt = episode.consolidatedAt ?? new Date().toISOString();
373
+ }
374
+ await Promise.all([
375
+ saveV2Episodes(this.v2Paths, v2Episodes),
376
+ saveV2Semantic(this.v2Paths, nextSemantic),
377
+ ]);
378
+ for (const ep of episodes) {
379
+ if (ep.consolidated)
380
+ await persistEpisode(this.episodesDir, ep);
381
+ }
382
+ const meta = await loadMeta(this.metaPath);
383
+ meta.lastConsolidation = new Date().toISOString();
384
+ await writeJson(this.metaPath, meta);
385
+ return {
386
+ entries: newEntries,
387
+ stats: { episodesConsidered: unconsolidatedCount, added, updated, skipped },
388
+ };
389
+ }
390
+ // ─── Retrieval & Injection (Progressive Recall) ────────────
391
+ async getMemoryInjection(project, contextTags, scope) {
392
+ await this.autoReviveRelevantArchive([project, ...contextTags].join(" ").trim(), scope);
393
+ const [allKnowledge, allLessons, allEvents, allPrefs, allFacets, allEpisodes, allWork, allV2Episodes, allV2EpisodeFacets, allV2Semantic, allProcedural] = await Promise.all([
394
+ loadEntries(this.knowledgePath),
395
+ loadEntries(this.lessonsPath),
396
+ loadEntries(this.eventsPath),
397
+ loadEntries(this.preferencesPath),
398
+ loadEntries(this.facetsPath),
399
+ loadEpisodes(this.episodesDir),
400
+ loadWork(this.workPath),
401
+ loadV2Episodes(this.v2Paths),
402
+ loadV2Facets(this.v2Paths),
403
+ loadV2Semantic(this.v2Paths),
404
+ loadV2Procedural(this.v2Paths),
405
+ ]);
406
+ const semanticCandidates = await this.querySemanticCandidates([project, ...contextTags].join(" ").trim(), project, scope, this.filterAndCleanV2Episodes(allV2Episodes, project, scope), this.filterAndCleanV2Facets(allV2EpisodeFacets, project, scope), this.filterAndCleanV2Semantic(allV2Semantic, project, scope), this.filterAndCleanProcedural(allProcedural, project, scope));
407
+ const selection = selectRecallEntries({
408
+ knowledge: this.filterAndCleanEntries(allKnowledge, scope),
409
+ lessons: this.filterAndCleanEntries(allLessons, scope),
410
+ events: this.filterAndCleanEntries(allEvents, scope),
411
+ prefs: this.filterAndCleanEntries(allPrefs, scope),
412
+ facets: this.filterAndCleanEntries(allFacets, scope),
413
+ work: this.filterAndCleanWork(allWork, scope),
414
+ episodes: filterByScope(allEpisodes, scope),
415
+ v2Episodes: this.filterAndCleanV2Episodes(allV2Episodes, project, scope),
416
+ v2EpisodeFacets: this.filterAndCleanV2Facets(allV2EpisodeFacets, project, scope),
417
+ v2Semantic: this.filterAndCleanV2Semantic(allV2Semantic, project, scope),
418
+ procedural: this.filterAndCleanProcedural(allProcedural, project, scope),
419
+ allKnowledge, allLessons, allEvents, allPrefs, allFacets,
420
+ semanticCandidates,
421
+ halfLife: this.cfg.halfLife,
422
+ scoreWeights: this.cfg.scoreWeights,
423
+ progressiveRecall: this.cfg.progressiveRecall,
424
+ tokenBudget: this.cfg.tokenBudget,
425
+ structuralWeight: this.cfg.structuralWeight,
426
+ project, contextTags,
427
+ });
428
+ // Publish recall snapshot to turn-context bus for eval collection
429
+ this.publishRecallSnapshot(selection, project, contextTags);
430
+ // Reinforce all recalled entries via spaced repetition
431
+ await this.reinforceEntries(selection.injectedActiveKnowledge, allKnowledge, this.knowledgePath);
432
+ await this.reinforceEntries(selection.injectedActiveLessons, allLessons, this.lessonsPath);
433
+ await this.reinforceEntries(selection.injectedActiveEvents, allEvents, this.eventsPath);
434
+ await this.reinforceEntries(selection.injectedActivePrefs, allPrefs, this.preferencesPath);
435
+ await this.reinforceEntries(selection.legacyFacetBridgeActive, allFacets, this.facetsPath);
436
+ await this.reinforceWork(selection.cue.work, allWork);
437
+ await this.reinforceEpisodeMemories([...selection.active.episodeMemories, ...selection.cue.episodeMemories], allV2Episodes);
438
+ await this.reinforceEpisodeFacets([...selection.active.episodeFacets, ...selection.cue.episodeFacets], allV2EpisodeFacets);
439
+ await this.reinforceV2SemanticMemories([...selection.active.semanticMemories, ...selection.cue.semanticMemories], allV2Semantic);
440
+ await this.reinforceProcedural([...selection.active.procedural, ...selection.cue.procedural], allProcedural);
441
+ await this.reconsolidateV2AfterRecall(allV2Episodes, allV2EpisodeFacets, allProcedural, [...selection.active.episodeMemories, ...selection.cue.episodeMemories], [...selection.active.episodeFacets, ...selection.cue.episodeFacets], [...selection.active.procedural, ...selection.cue.procedural]);
442
+ if (this.llmFn) {
443
+ await this.reconsolidateIfNeeded(selection.injectedActiveKnowledge, contextTags, allKnowledge);
444
+ await this.reconsolidateIfNeeded(selection.injectedActiveLessons, contextTags, allLessons);
445
+ }
446
+ const p = PROMPTS[this.cfg.locale] ?? PROMPTS.en;
447
+ return this.buildProgressiveInjectionText(selection.active, selection.cue, selection.allEntries, selection.graphContext, p);
448
+ }
449
+ // ─── Recall snapshot for eval collection ──────────────────
450
+ /**
451
+ * Build per-memory score breakdowns from the selection result and publish
452
+ * to the turn-context bus. The SAL extension (or any eval consumer) can
453
+ * read this snapshot from getTurnContext("memoryRecallSnapshot") in its
454
+ * agent_end hook to emit eval events. mem-core itself never imports from
455
+ * SAL or the eval system.
456
+ */
457
+ publishRecallSnapshot(selection, project, contextTags) {
458
+ const sw = this.cfg.structuralWeight;
459
+ const records = [];
460
+ const injectionOrder = buildInjectedMemoryOrder(selection.active, selection.cue);
461
+ const injectOrderByMemory = new Map(injectionOrder.map((record, index) => [`${record.memoryKind}:${record.memoryId}`, index + 1]));
462
+ // V2 episode memories
463
+ const activeEpIds = new Set(selection.active.episodeMemories.map((e) => e.id));
464
+ const allEpisodes = [...selection.active.episodeMemories, ...selection.cue.episodeMemories];
465
+ for (const entry of allEpisodes) {
466
+ const bd = breakdownEpisodeMemory(entry, project, contextTags, sw);
467
+ const injected = activeEpIds.has(entry.id);
468
+ records.push({
469
+ memoryId: entry.id, memoryKind: "episode",
470
+ anchorModule: entry.structuralAnchor?.modulePath,
471
+ anchorFile: entry.structuralAnchor?.filePath,
472
+ scoreBreakdownStatus: "available",
473
+ scoreRecency: bd.recency, scoreImportance: bd.importance,
474
+ scoreRelevance: bd.relevance, scoreStructural: bd.structural,
475
+ scoreFinal: bd.final, wasInjected: injected,
476
+ });
477
+ }
478
+ // V2 episode facets
479
+ const activeEfIds = new Set(selection.active.episodeFacets.map((e) => e.id));
480
+ const allFacets = [...selection.active.episodeFacets, ...selection.cue.episodeFacets];
481
+ for (const entry of allFacets) {
482
+ const bd = breakdownEpisodeFacet(entry, project, contextTags, sw);
483
+ const injected = activeEfIds.has(entry.id);
484
+ records.push({
485
+ memoryId: entry.id, memoryKind: "episodeFacet",
486
+ anchorModule: entry.structuralAnchor?.modulePath,
487
+ anchorFile: entry.structuralAnchor?.filePath,
488
+ scoreBreakdownStatus: "available",
489
+ scoreRecency: bd.recency, scoreImportance: bd.importance,
490
+ scoreRelevance: bd.relevance, scoreStructural: bd.structural,
491
+ scoreFinal: bd.final, wasInjected: injected,
492
+ });
493
+ }
494
+ // V2 semantic memories
495
+ const activeSmIds = new Set(selection.active.semanticMemories.map((e) => e.id));
496
+ const allSemantic = [...selection.active.semanticMemories, ...selection.cue.semanticMemories];
497
+ for (const entry of allSemantic) {
498
+ const bd = breakdownV2Semantic(entry, project, contextTags, sw);
499
+ const injected = activeSmIds.has(entry.id);
500
+ records.push({
501
+ memoryId: entry.id, memoryKind: "semantic",
502
+ anchorModule: entry.structuralAnchor?.modulePath,
503
+ anchorFile: entry.structuralAnchor?.filePath,
504
+ scoreBreakdownStatus: "available",
505
+ scoreRecency: bd.recency, scoreImportance: bd.importance,
506
+ scoreRelevance: bd.relevance, scoreStructural: bd.structural,
507
+ scoreFinal: bd.final, wasInjected: injected,
508
+ });
509
+ }
510
+ // V2 procedural memories
511
+ const activePrIds = new Set(selection.active.procedural.map((e) => e.id));
512
+ const allProcedural = [...selection.active.procedural, ...selection.cue.procedural];
513
+ for (const entry of allProcedural) {
514
+ const bd = breakdownProcedural(entry, project, contextTags, sw);
515
+ const injected = activePrIds.has(entry.id);
516
+ records.push({
517
+ memoryId: entry.id, memoryKind: "procedural",
518
+ anchorModule: entry.structuralAnchor?.modulePath,
519
+ anchorFile: entry.structuralAnchor?.filePath,
520
+ scoreBreakdownStatus: "available",
521
+ scoreRecency: bd.recency, scoreImportance: bd.importance,
522
+ scoreRelevance: bd.relevance, scoreStructural: bd.structural,
523
+ scoreFinal: bd.final, wasInjected: injected,
524
+ });
525
+ }
526
+ // Legacy entries (knowledge/lessons/events/prefs/facets) — no structural anchor
527
+ const legacyKinds = [
528
+ { active: selection.injectedActiveKnowledge, cue: selection.injectedCueKnowledge, kind: "knowledge" },
529
+ { active: selection.injectedActiveLessons, cue: selection.injectedCueLessons, kind: "lesson" },
530
+ { active: selection.injectedActiveEvents, cue: selection.injectedCueEvents, kind: "event" },
531
+ { active: selection.injectedActivePrefs, cue: selection.injectedCuePrefs, kind: "preference" },
532
+ { active: selection.legacyFacetBridgeActive, cue: selection.legacyFacetBridgeCue, kind: "facet" },
533
+ ];
534
+ const hl = this.cfg.halfLife;
535
+ const scoreWeights = this.cfg.scoreWeights;
536
+ for (const { active, cue, kind } of legacyKinds) {
537
+ const activeIds = new Set(active.map((e) => e.id));
538
+ for (const entry of [...active, ...cue]) {
539
+ const score = scoreEntry(entry, project, contextTags, hl, scoreWeights);
540
+ records.push({
541
+ memoryId: entry.id, memoryKind: kind,
542
+ scoreBreakdownStatus: "unavailable",
543
+ scoreFinal: score, wasInjected: activeIds.has(entry.id),
544
+ });
545
+ }
546
+ }
547
+ // Assign inject_rank by actual injection order, matching prompt assembly order.
548
+ for (const record of records) {
549
+ if (!record.wasInjected)
550
+ continue;
551
+ record.injectRank = injectOrderByMemory.get(`${record.memoryKind}:${record.memoryId}`);
552
+ }
553
+ setTurnContext("memoryRecallSnapshot", records);
554
+ }
555
+ // ─── Progressive Recall Tools ───────────────────────────────
556
+ /** Retrieve a single entry by ID (for recall_memory tool) */
557
+ async recallV2ById(id) {
558
+ const snapshot = await this.getV2Snapshot();
559
+ const semantic = snapshot.semantic.find(entry => entry.id === id);
560
+ if (semantic) {
561
+ await this.reinforceV2SemanticMemories([semantic], snapshot.semantic);
562
+ return semantic;
563
+ }
564
+ const episode = snapshot.episodes.find(entry => entry.id === id);
565
+ if (episode) {
566
+ await this.reinforceEpisodeMemories([episode], snapshot.episodes);
567
+ return episode;
568
+ }
569
+ const facet = snapshot.facets.find(entry => entry.id === id);
570
+ if (facet) {
571
+ await this.reinforceEpisodeFacets([facet], snapshot.facets);
572
+ return facet;
573
+ }
574
+ const procedural = snapshot.procedural.find(entry => entry.id === id);
575
+ if (procedural) {
576
+ await this.reinforceProcedural([procedural], snapshot.procedural);
577
+ return procedural;
578
+ }
579
+ const state = await loadV2State(this.v2Paths);
580
+ return state.find(entry => entry.id === id) ?? null;
581
+ }
582
+ async getEntryById(id) {
583
+ for (const { path } of this.getMemoryPathConfigs()) {
584
+ const entries = await loadEntries(path);
585
+ const entry = entries.find((e) => e.id === id);
586
+ if (entry)
587
+ return entry;
588
+ }
589
+ const semantic = (await loadV2Semantic(this.v2Paths)).find(entry => entry.id === id);
590
+ if (semantic)
591
+ return this.semanticToRuntimeEntry(semantic);
592
+ const procedural = (await loadV2Procedural(this.v2Paths)).find(entry => entry.id === id);
593
+ return procedural ? this.proceduralToRuntimeEntry(procedural) : null;
594
+ }
595
+ /** Reinforce a single entry by ID (bump accessCount, lastAccessed, strength) */
596
+ async reinforceEntryById(id) {
597
+ const pathConfigs = this.getMemoryPathConfigs();
598
+ const hl = this.cfg.halfLife;
599
+ const ew = this.cfg.evictionWeights;
600
+ for (const { path, max } of pathConfigs) {
601
+ const entries = await loadEntries(path);
602
+ const entry = entries.find((e) => e.id === id);
603
+ if (entry) {
604
+ entry.accessCount = (entry.accessCount ?? 0) + 1;
605
+ entry.lastAccessed = new Date().toISOString();
606
+ entry.strength = (entry.strength || 30) * this.cfg.strengthGrowthFactor;
607
+ await saveEntries(path, entries, max, (e) => utilityEntry(e, hl, ew));
608
+ return true;
609
+ }
610
+ }
611
+ return Boolean(await this.recallV2ById(id));
612
+ }
613
+ async editEntryById(id, patch) {
614
+ const located = await this.findEntryLocation(id);
615
+ if (!located) {
616
+ const semantic = await loadV2Semantic(this.v2Paths);
617
+ const item = semantic.find(entry => entry.id === id);
618
+ if (item) {
619
+ const legacy = Object.values(await this.getAllEntries()).flat().find(entry => (item.legacyIds?.includes(entry.id) || (entry.name === item.name && entry.summary === item.summary && entry.project === item.scope?.project)));
620
+ if (legacy) {
621
+ await this.editEntryById(legacy.id, patch);
622
+ return this.getEntryById(id);
623
+ }
624
+ Object.assign(item, patch, { ttlDays: patch.ttl ?? item.ttlDays, updatedAt: new Date().toISOString() });
625
+ item.tags = patch.tags ?? extractTags(`${item.name} ${item.summary} ${item.detail || ''}`);
626
+ await saveV2Semantic(this.v2Paths, semantic);
627
+ return this.semanticToRuntimeEntry(item);
628
+ }
629
+ const procedural = await loadV2Procedural(this.v2Paths);
630
+ const procedure = procedural.find(entry => entry.id === id);
631
+ if (!procedure)
632
+ return null;
633
+ Object.assign(procedure, patch, { ttlDays: patch.ttl ?? procedure.ttlDays, updatedAt: new Date().toISOString() });
634
+ if (patch.detail !== undefined)
635
+ procedure.contextText = patch.detail;
636
+ procedure.searchText = `${procedure.name} ${procedure.summary} ${procedure.contextText || ''}`;
637
+ procedure.tags = patch.tags ?? extractTags(procedure.searchText);
638
+ await saveV2Procedural(this.v2Paths, procedural);
639
+ return this.proceduralToRuntimeEntry(procedure);
640
+ }
641
+ const { entry, entries, path, max } = located;
642
+ const previousName = entry.name;
643
+ const previousSummary = entry.summary;
644
+ const detail = patch.detail ?? entry.detail ?? entry.content ?? "";
645
+ entry.name = patch.name ?? entry.name;
646
+ entry.summary = patch.summary ?? entry.summary;
647
+ entry.detail = detail;
648
+ entry.content = detail;
649
+ entry.retention = patch.retention ?? entry.retention;
650
+ entry.salience = patch.salience ?? entry.salience;
651
+ entry.stability = patch.stability ?? entry.stability;
652
+ entry.ttl = patch.ttl ?? entry.ttl;
653
+ entry.tags = patch.tags?.length ? patch.tags : extractTags(`${entry.name || ""} ${entry.summary || ""} ${detail}`);
654
+ entry.lastAccessed = new Date().toISOString();
655
+ await this.persistEntries(path, entries, max);
656
+ for (const v2Paths of [this.v2Paths, this.archiveV2Paths]) {
657
+ const semantic = await loadV2Semantic(v2Paths);
658
+ const mirror = semantic.find((item) => (item.legacyIds?.includes(id) || (item.name === previousName && item.summary === previousSummary && item.scope?.project === entry.project)));
659
+ if (!mirror)
660
+ continue;
661
+ mirror.legacyIds = [...new Set([...(mirror.legacyIds ?? []), id])];
662
+ mirror.name = entry.name || mirror.name;
663
+ mirror.summary = entry.summary || mirror.summary;
664
+ mirror.detail = entry.detail;
665
+ mirror.tags = entry.tags;
666
+ mirror.retention = entry.retention || mirror.retention;
667
+ mirror.salience = entry.salience ?? mirror.salience;
668
+ mirror.stability = entry.stability || mirror.stability;
669
+ mirror.ttlDays = entry.ttl;
670
+ mirror.updatedAt = new Date().toISOString();
671
+ await saveV2Semantic(v2Paths, semantic);
672
+ }
673
+ return entry;
674
+ }
675
+ async resolveConflictByIds(aId, bId, action) {
676
+ const a = await this.getEntryById(aId);
677
+ const b = await this.getEntryById(bId);
678
+ if (!a || !b || aId === bId)
679
+ return null;
680
+ const score = (entry) => (entry.salience ?? entry.importance) * 2 + (entry.accessCount ?? 0) + (entry.retention === "core" ? 4 : entry.retention === "key-event" ? 2 : 0);
681
+ const [primary, secondary] = score(a) >= score(b) ? [a, b] : [b, a];
682
+ if (action === "forget") {
683
+ await this.forgetEntry(secondary.id);
684
+ return { action, updatedIds: [primary.id] };
685
+ }
686
+ if (action === "demote" || action === "mark-situational") {
687
+ await this.editEntryById(secondary.id, {
688
+ retention: "ambient",
689
+ salience: action === "demote" ? Math.max(1, Math.min(secondary.salience ?? secondary.importance, 4)) : secondary.salience,
690
+ stability: action === "mark-situational" ? "situational" : secondary.stability,
691
+ ttl: action === "mark-situational" ? Math.min(14, this.cfg.forgetting.ambientTtlDays) : secondary.ttl ?? this.cfg.forgetting.ambientTtlDays,
692
+ });
693
+ return { action, updatedIds: [secondary.id] };
694
+ }
695
+ await this.editEntryById(primary.id, {
696
+ summary: [primary.summary, secondary.summary].filter(Boolean).join(" | ").slice(0, 300),
697
+ detail: [primary.detail || primary.content, secondary.detail || secondary.content].filter(Boolean).join("\n\n").slice(0, 4000),
698
+ tags: [...new Set([...(primary.tags ?? []), ...(secondary.tags ?? [])])].slice(0, 30),
699
+ salience: Math.max(primary.salience ?? primary.importance, secondary.salience ?? secondary.importance),
700
+ });
701
+ await this.forgetEntry(secondary.id);
702
+ return { action, updatedIds: [primary.id] };
703
+ }
704
+ /** Full-text search across ALL entries including dormant (for search_all_memories tool) */
705
+ async searchAllEntries(query, limit = 10) {
706
+ const tags = extractTags(query);
707
+ const queryLower = query.toLowerCase();
708
+ const [knowledge, lessons, events, prefs, facets] = await Promise.all([
709
+ loadEntries(this.knowledgePath),
710
+ loadEntries(this.lessonsPath),
711
+ loadEntries(this.eventsPath),
712
+ loadEntries(this.preferencesPath),
713
+ loadEntries(this.facetsPath),
714
+ ]);
715
+ const all = [...knowledge, ...lessons, ...events, ...prefs, ...facets];
716
+ return all
717
+ .map((e) => {
718
+ const nameMatch = (e.name || "").toLowerCase().includes(queryLower) ? 2 : 0;
719
+ const summaryMatch = (e.summary || "").toLowerCase().includes(queryLower) ? 1 : 0;
720
+ const tagMatch = tagOverlap(e.tags, tags);
721
+ const relationBoost = (e.relations ?? [])
722
+ .filter((relation) => tags.some((tag) => relation.kind.includes(tag) || relation.id.includes(tag)))
723
+ .reduce((sum, relation) => sum + relation.weight * 0.15, 0);
724
+ return { entry: e, score: nameMatch + summaryMatch + tagMatch + relationBoost };
725
+ })
726
+ .filter((x) => x.score > 0)
727
+ .sort((a, b) => b.score - a.score)
728
+ .slice(0, limit)
729
+ .map((x) => x.entry);
730
+ }
731
+ // ─── Stats ───────────────────────────────────────────────────
732
+ async getStats() {
733
+ const [knowledge, lessons, events, prefs, facets, episodes, work, meta, archived] = await Promise.all([
734
+ loadEntries(this.knowledgePath),
735
+ loadEntries(this.lessonsPath),
736
+ loadEntries(this.eventsPath),
737
+ loadEntries(this.preferencesPath),
738
+ loadEntries(this.facetsPath),
739
+ loadEpisodes(this.episodesDir),
740
+ loadWork(this.workPath),
741
+ loadMeta(this.metaPath),
742
+ this.exportArchive(),
743
+ ]);
744
+ return {
745
+ knowledge: knowledge.length,
746
+ lessons: lessons.length,
747
+ events: events.length,
748
+ preferences: prefs.length,
749
+ facets: facets.length,
750
+ episodes: episodes.length,
751
+ work: work.length,
752
+ archivedKnowledge: archived.knowledge.length,
753
+ archivedLessons: archived.lessons.length,
754
+ archivedEvents: archived.events.length,
755
+ archivedPreferences: archived.preferences.length,
756
+ archivedFacets: archived.facets.length,
757
+ archivedWork: archived.work.length,
758
+ totalSessions: meta.totalSessions,
759
+ };
760
+ }
761
+ async getV2Stats() {
762
+ const [episodes, facets, semantic, procedural, links, embeddingIndex, meta, archived] = await Promise.all([
763
+ loadV2Episodes(this.v2Paths),
764
+ loadV2Facets(this.v2Paths),
765
+ loadV2Semantic(this.v2Paths),
766
+ loadV2Procedural(this.v2Paths),
767
+ loadV2Links(this.v2Paths),
768
+ loadEmbeddingIndex(this.cfg.memoryDir),
769
+ loadV2Meta(this.v2Paths),
770
+ this.exportArchive(),
771
+ ]);
772
+ return {
773
+ episodes: episodes.length,
774
+ facets: facets.length,
775
+ semantic: semantic.length,
776
+ procedural: procedural.length,
777
+ archivedSemantic: archived.semantic.length,
778
+ archivedProcedural: archived.procedural.length,
779
+ links: links.length,
780
+ embeddings: embeddingIndex.records.length,
781
+ lastEmbeddingSyncAt: meta.lastEmbeddingSyncAt,
782
+ lastReconsolidationAt: meta.lastReconsolidationAt,
783
+ };
784
+ }
785
+ // ─── Direct Access (for CLI, testing) ────────────────────────
786
+ async getAllEntries() {
787
+ return {
788
+ knowledge: await loadEntries(this.knowledgePath),
789
+ lessons: await loadEntries(this.lessonsPath),
790
+ events: await loadEntries(this.eventsPath),
791
+ preferences: await loadEntries(this.preferencesPath),
792
+ facets: await loadEntries(this.facetsPath),
793
+ };
794
+ }
795
+ async getRuntimeIdentityEntries() {
796
+ const runtime = await this.buildRuntimeMemoryView();
797
+ return {
798
+ knowledge: runtime.knowledge,
799
+ lessons: runtime.lessons,
800
+ preferences: runtime.preferences,
801
+ facets: runtime.facets,
802
+ };
803
+ }
804
+ async getAllWork() {
805
+ return loadWork(this.workPath);
806
+ }
807
+ async getAllEpisodes() {
808
+ return loadEpisodes(this.episodesDir);
809
+ }
810
+ async runStartupMaintenance(maintenanceVersion = 3) {
811
+ const [meta, v2Meta] = await Promise.all([loadMeta(this.metaPath), loadV2Meta(this.v2Paths)]);
812
+ const alreadyMaintained = (meta.lastMaintenanceVersion ?? 0) >= maintenanceVersion &&
813
+ (v2Meta.lastMaintenanceVersion ?? 0) >= maintenanceVersion;
814
+ if (alreadyMaintained) {
815
+ return {
816
+ ran: false,
817
+ backupPath: undefined,
818
+ deduplicated: { knowledge: 0, lessons: 0, events: 0, preferences: 0, facets: 0, work: 0, total: 0 },
819
+ migratedEpisodesToV2: 0,
820
+ archived: { knowledge: 0, lessons: 0, events: 0, preferences: 0, facets: 0, work: 0, semantic: 0, procedural: 0, total: 0 },
821
+ };
822
+ }
823
+ const now = new Date().toISOString();
824
+ const backupPath = await this.createMaintenanceBackup(meta, v2Meta, maintenanceVersion, now);
825
+ const deduplicated = await this.deduplicateAll();
826
+ const episodes = await this.getAllEpisodes();
827
+ for (const episode of episodes) {
828
+ await this.syncEpisodeToV2(episode);
829
+ }
830
+ await this.rebuildV2Links();
831
+ const archived = await this.archiveStaleMemories();
832
+ await this.rebuildV2Links();
833
+ await Promise.all([
834
+ writeJson(this.metaPath, {
835
+ ...(await loadMeta(this.metaPath)),
836
+ lastMaintenanceAt: now,
837
+ lastMaintenanceVersion: maintenanceVersion,
838
+ lastBackupAt: meta.lastBackupAt ?? now,
839
+ lastBackupVersion: Math.max(meta.lastBackupVersion ?? 0, maintenanceVersion),
840
+ }),
841
+ saveV2Meta(this.v2Paths, {
842
+ ...(await loadV2Meta(this.v2Paths)),
843
+ version: 2,
844
+ lastMaintenanceAt: now,
845
+ lastMaintenanceVersion: maintenanceVersion,
846
+ lastMigrationAt: (await loadV2Meta(this.v2Paths)).lastMigrationAt ?? now,
847
+ lastBackupAt: v2Meta.lastBackupAt ?? now,
848
+ lastBackupVersion: Math.max(v2Meta.lastBackupVersion ?? 0, maintenanceVersion),
849
+ }),
850
+ ]);
851
+ return {
852
+ ran: true,
853
+ backupPath,
854
+ deduplicated,
855
+ migratedEpisodesToV2: episodes.length,
856
+ archived,
857
+ };
858
+ }
859
+ async createMaintenanceBackup(meta, v2Meta, maintenanceVersion, now) {
860
+ const alreadyBackedUp = (meta.lastBackupVersion ?? 0) >= maintenanceVersion &&
861
+ (v2Meta.lastBackupVersion ?? 0) >= maintenanceVersion;
862
+ if (alreadyBackedUp)
863
+ return undefined;
864
+ const safeTimestamp = now.replace(/[:.]/g, "-");
865
+ const backupRoot = join(this.cfg.memoryDir, "_backups");
866
+ const backupDir = join(backupRoot, `maintenance-v${maintenanceVersion}-${safeTimestamp}`);
867
+ await mkdir(backupDir, { recursive: true });
868
+ const filesToCopy = [
869
+ this.knowledgePath,
870
+ this.lessonsPath,
871
+ this.eventsPath,
872
+ this.preferencesPath,
873
+ this.facetsPath,
874
+ this.workPath,
875
+ this.metaPath,
876
+ ];
877
+ for (const filePath of filesToCopy) {
878
+ try {
879
+ await cp(filePath, join(backupDir, filePath.split("/").pop() ?? "unknown.json"));
880
+ }
881
+ catch {
882
+ // Missing files are fine for first-run users.
883
+ }
884
+ }
885
+ for (const dirName of ["episodes", "v2", "_archive"]) {
886
+ const sourceDir = join(this.cfg.memoryDir, dirName);
887
+ try {
888
+ await readdir(sourceDir);
889
+ await cp(sourceDir, join(backupDir, dirName), { recursive: true });
890
+ }
891
+ catch {
892
+ // Skip directories that do not exist yet.
893
+ }
894
+ }
895
+ await writeJson(join(backupDir, "manifest.json"), {
896
+ createdAt: now,
897
+ maintenanceVersion,
898
+ memoryDir: this.cfg.memoryDir,
899
+ });
900
+ return backupDir;
901
+ }
902
+ async syncEpisodeToV2(ep) {
903
+ const [episodes, facets, links, procedural, meta] = await Promise.all([
904
+ loadV2Episodes(this.v2Paths),
905
+ loadV2Facets(this.v2Paths),
906
+ loadV2Links(this.v2Paths),
907
+ loadV2Procedural(this.v2Paths),
908
+ loadV2Meta(this.v2Paths),
909
+ ]);
910
+ const now = new Date().toISOString();
911
+ const episodeId = this.makeEpisodeMemoryId(ep);
912
+ const mapped = this.mapEpisodeToV2(ep, now);
913
+ const nextEpisodes = episodes.filter((existing) => existing.id !== episodeId);
914
+ nextEpisodes.push(mapped.episode);
915
+ const nextFacets = facets.filter((facet) => facet.episodeId !== episodeId);
916
+ nextFacets.push(...mapped.facets);
917
+ const facetIds = new Set(mapped.facets.map((facet) => facet.id));
918
+ const nextLinks = links.filter((link) => link.fromId !== episodeId &&
919
+ link.toId !== episodeId &&
920
+ !facetIds.has(link.fromId) &&
921
+ !facetIds.has(link.toId));
922
+ nextLinks.push(...mapped.links);
923
+ const nextProcedural = procedural.filter((item) => !item.sourceEpisodeIds?.includes(episodeId));
924
+ const compiledProcedure = compileProcedureFromEpisode(mapped.episode, mapped.facets, now);
925
+ if (compiledProcedure) {
926
+ nextProcedural.push(compiledProcedure);
927
+ mapped.episode.derivedProcedureIds = [compiledProcedure.id];
928
+ nextEpisodes[nextEpisodes.length - 1] = mapped.episode;
929
+ }
930
+ await Promise.all([
931
+ saveV2Episodes(this.v2Paths, nextEpisodes),
932
+ saveV2Facets(this.v2Paths, nextFacets),
933
+ saveV2Links(this.v2Paths, nextLinks),
934
+ saveV2Procedural(this.v2Paths, nextProcedural),
935
+ saveV2Meta(this.v2Paths, {
936
+ ...meta,
937
+ version: 2,
938
+ lastMigrationAt: meta.lastMigrationAt ?? now,
939
+ }),
940
+ ]);
941
+ if (this.cfg.embeddings.enabled && this.cfg.embeddings.autoSync && this.embeddingFn) {
942
+ await this.syncV2Embeddings();
943
+ }
944
+ }
945
+ mapEpisodeToV2(ep, now) {
946
+ const episodeId = this.makeEpisodeMemoryId(ep);
947
+ const baseTags = [...new Set([...(ep.tags ?? []), ...extractTags(`${ep.project} ${ep.summary} ${ep.userGoal ?? ""}`)])];
948
+ const facetDefs = [];
949
+ if (ep.userGoal?.trim()) {
950
+ facetDefs.push({
951
+ facetType: "goal",
952
+ searchText: ep.userGoal.trim(),
953
+ anchorText: ep.summary,
954
+ summary: `Goal: ${ep.userGoal.trim()}`,
955
+ });
956
+ }
957
+ for (const error of ep.errors.slice(0, 8)) {
958
+ if (!error.trim())
959
+ continue;
960
+ facetDefs.push({
961
+ facetType: "error",
962
+ searchText: error.trim(),
963
+ anchorText: ep.summary,
964
+ summary: `Error: ${error.trim()}`,
965
+ causalRole: "signal",
966
+ outcomeScore: Math.max(1, 6 - ep.errors.length),
967
+ });
968
+ }
969
+ for (const observation of ep.keyObservations.slice(0, 8)) {
970
+ if (!observation.trim())
971
+ continue;
972
+ facetDefs.push({
973
+ facetType: "insight",
974
+ searchText: observation.trim(),
975
+ anchorText: ep.summary,
976
+ summary: `Insight: ${observation.trim()}`,
977
+ causalRole: "resolution",
978
+ outcomeScore: Math.min(10, ep.importance),
979
+ });
980
+ }
981
+ if (ep.summary.trim()) {
982
+ facetDefs.push({
983
+ facetType: "outcome",
984
+ searchText: ep.summary.trim().slice(0, 200),
985
+ anchorText: ep.summary.trim(),
986
+ summary: ep.summary.trim(),
987
+ detail: [ep.userGoal ? `Goal: ${ep.userGoal}` : "", ep.errors.length ? `Errors: ${ep.errors.join("; ")}` : ""]
988
+ .filter(Boolean)
989
+ .join("\n"),
990
+ causalRole: ep.errors.length > 0 ? "effect" : "signal",
991
+ outcomeScore: ep.errors.length > 0 ? Math.max(3, ep.importance - 1) : ep.importance,
992
+ });
993
+ }
994
+ const facets = facetDefs.map((facet, index) => {
995
+ const facetId = `${episodeId}:facet:${index + 1}`;
996
+ const facetTags = [
997
+ ...new Set([
998
+ ...baseTags,
999
+ facet.facetType,
1000
+ ...extractTags(`${facet.searchText} ${facet.summary ?? ""} ${facet.anchorText ?? ""}`),
1001
+ ]),
1002
+ ].slice(0, 40);
1003
+ return {
1004
+ id: facetId,
1005
+ kind: "facet",
1006
+ episodeId,
1007
+ facetType: facet.facetType,
1008
+ searchText: facet.searchText,
1009
+ anchorText: facet.anchorText,
1010
+ summary: facet.summary,
1011
+ detail: facet.detail,
1012
+ accessCount: 0,
1013
+ importance: Math.max(1, Math.min(10, ep.importance)),
1014
+ salience: Math.max(1, Math.min(10, facet.outcomeScore ?? ep.importance)),
1015
+ confidence: 0.7,
1016
+ retention: ep.importance >= 8 ? "key-event" : "ambient",
1017
+ stability: ep.errors.length > 0 ? "situational" : "stable",
1018
+ tags: facetTags,
1019
+ sourceEpisodeIds: [episodeId],
1020
+ evidence: [],
1021
+ scope: { ...ep.scope, project: ep.project },
1022
+ createdAt: ep.startedAt ?? `${ep.date}T00:00:00.000Z`,
1023
+ updatedAt: now,
1024
+ entityRefs: [],
1025
+ aliases: [],
1026
+ causalRole: facet.causalRole,
1027
+ outcomeScore: facet.outcomeScore,
1028
+ structuralAnchor: this.currentStructuralAnchor(),
1029
+ };
1030
+ });
1031
+ const links = facets.map((facet) => ({
1032
+ id: `${episodeId}->${facet.id}`,
1033
+ fromId: episodeId,
1034
+ toId: facet.id,
1035
+ type: "has-facet",
1036
+ weight: 1,
1037
+ explicit: true,
1038
+ createdAt: now,
1039
+ updatedAt: now,
1040
+ evidence: [],
1041
+ }));
1042
+ const episode = {
1043
+ id: episodeId,
1044
+ kind: "episode",
1045
+ sessionId: ep.sessionId,
1046
+ title: ep.userGoal || ep.summary.slice(0, 80),
1047
+ summary: ep.summary,
1048
+ startedAt: ep.startedAt,
1049
+ endedAt: ep.endedAt,
1050
+ timeZone: ep.timeZone,
1051
+ userGoal: ep.userGoal,
1052
+ outcome: ep.errors.length ? "high-friction" : "completed",
1053
+ accessCount: 0,
1054
+ importance: Math.max(1, Math.min(10, ep.importance)),
1055
+ salience: Math.max(1, Math.min(10, ep.importance + Math.min(ep.errors.length, 2))),
1056
+ confidence: 0.85,
1057
+ retention: ep.importance >= 8 ? "key-event" : "ambient",
1058
+ stability: ep.errors.length > 0 ? "situational" : "stable",
1059
+ tags: baseTags.slice(0, 40),
1060
+ sourceEpisodeIds: [episodeId],
1061
+ evidence: [],
1062
+ scope: { ...ep.scope, project: ep.project },
1063
+ createdAt: ep.startedAt ?? `${ep.date}T00:00:00.000Z`,
1064
+ updatedAt: now,
1065
+ filesModified: ep.filesModified,
1066
+ toolsUsed: ep.toolsUsed,
1067
+ entities: [],
1068
+ facetIds: facets.map((facet) => facet.id),
1069
+ derivedSemanticIds: [],
1070
+ derivedProcedureIds: [],
1071
+ consolidatedAt: ep.consolidated ? now : undefined,
1072
+ structuralAnchor: this.currentStructuralAnchor(),
1073
+ };
1074
+ return { episode, facets, links };
1075
+ }
1076
+ makeEpisodeMemoryId(ep) {
1077
+ return `episode:${ep.sessionId}`;
1078
+ }
1079
+ /** Deduplicate all memory and work entries, merge relatedIds, save. Returns counts of removed duplicates. */
1080
+ async deduplicateAll() {
1081
+ const [knowledge, lessons, events, prefs, facets, work] = await Promise.all([
1082
+ loadEntries(this.knowledgePath),
1083
+ loadEntries(this.lessonsPath),
1084
+ loadEntries(this.eventsPath),
1085
+ loadEntries(this.preferencesPath),
1086
+ loadEntries(this.facetsPath),
1087
+ loadWork(this.workPath),
1088
+ ]);
1089
+ const k = deduplicateMemoryEntries(knowledge);
1090
+ const l = deduplicateMemoryEntries(lessons);
1091
+ const ev = deduplicateMemoryEntries(events);
1092
+ const p = deduplicateMemoryEntries(prefs);
1093
+ const f = deduplicateMemoryEntries(facets);
1094
+ const w = deduplicateWorkEntries(work);
1095
+ const hl = this.cfg.halfLife;
1096
+ const ew = this.cfg.evictionWeights;
1097
+ await Promise.all([
1098
+ saveEntries(this.knowledgePath, k.deduped, this.cfg.maxEntries.knowledge, (e) => utilityEntry(e, hl, ew)),
1099
+ saveEntries(this.lessonsPath, l.deduped, this.cfg.maxEntries.lessons, (e) => utilityEntry(e, hl, ew)),
1100
+ saveEntries(this.eventsPath, ev.deduped, this.cfg.maxEntries.events, (e) => utilityEntry(e, hl, ew)),
1101
+ saveEntries(this.preferencesPath, p.deduped, this.cfg.maxEntries.preferences, (e) => utilityEntry(e, hl, ew)),
1102
+ saveEntries(this.facetsPath, f.deduped, this.cfg.maxEntries.facets, (e) => utilityEntry(e, hl, ew)),
1103
+ saveWork(this.workPath, w.deduped, this.cfg.maxEntries.work, (wr) => utilityWork(wr, this.cfg.halfLife, this.cfg.evictionWeights)),
1104
+ ]);
1105
+ const total = k.removedCount + l.removedCount + ev.removedCount + p.removedCount + f.removedCount + w.removedCount;
1106
+ return {
1107
+ knowledge: k.removedCount,
1108
+ lessons: l.removedCount,
1109
+ events: ev.removedCount,
1110
+ preferences: p.removedCount,
1111
+ facets: f.removedCount,
1112
+ work: w.removedCount,
1113
+ total,
1114
+ };
1115
+ }
1116
+ async searchEntries(query, scope) {
1117
+ await this.autoReviveRelevantArchive(query, scope);
1118
+ const runtime = await this.buildRuntimeMemoryView();
1119
+ const tags = extractTags(query);
1120
+ const v2Primary = runtime.v2SearchEntries
1121
+ .map((entry) => ({
1122
+ entry,
1123
+ score: scoreEntry(entry, entry.project, tags, this.cfg.halfLife, this.cfg.scoreWeights),
1124
+ }))
1125
+ .filter((item) => item.score >= 0.42)
1126
+ .sort((a, b) => b.score - a.score)
1127
+ .map((item) => item.entry);
1128
+ if (v2Primary.length > 0) {
1129
+ const bridgeFacets = runtime.facets
1130
+ .filter((entry) => entry.facetData?.kind === "pattern" || entry.facetData?.kind === "struggle")
1131
+ .map((entry) => ({
1132
+ entry,
1133
+ score: scoreEntry(entry, entry.project, tags, this.cfg.halfLife, this.cfg.scoreWeights),
1134
+ }))
1135
+ .filter((item) => item.score >= 0.38)
1136
+ .sort((a, b) => b.score - a.score)
1137
+ .map((item) => item.entry);
1138
+ return [...v2Primary, ...bridgeFacets].slice(0, 12);
1139
+ }
1140
+ const all = [...runtime.knowledge, ...runtime.lessons, ...runtime.events, ...runtime.preferences, ...runtime.facets];
1141
+ return all
1142
+ .map((entry) => ({
1143
+ entry,
1144
+ score: tagOverlap(entry.tags, tags) +
1145
+ (entry.relations ?? [])
1146
+ .filter((relation) => tags.some((tag) => relation.kind.includes(tag) || relation.id.includes(tag)))
1147
+ .reduce((sum, relation) => sum + relation.weight * 0.12, 0),
1148
+ }))
1149
+ .filter((item) => item.score > 0)
1150
+ .sort((a, b) => b.score - a.score)
1151
+ .map((item) => item.entry);
1152
+ }
1153
+ async autoReviveRelevantArchive(query, scope, options) {
1154
+ const trimmed = query.trim();
1155
+ const contextTags = extractTags(trimmed);
1156
+ if (!trimmed || contextTags.length === 0)
1157
+ return [];
1158
+ const project = scope?.project ?? "";
1159
+ const hl = this.cfg.halfLife;
1160
+ const sw = this.cfg.scoreWeights;
1161
+ const maxItems = options?.maxItems ?? NanoMemEngine.AUTO_REVIVE_MAX_ITEMS;
1162
+ const legacyThreshold = options?.legacyThreshold ?? 0.82;
1163
+ const v2Threshold = options?.v2Threshold ?? 0.8;
1164
+ const revived = [];
1165
+ const archive = await this.exportArchive();
1166
+ const legacyCandidates = [
1167
+ ...archive.knowledge.map((entry) => ({ entry, location: "knowledge" })),
1168
+ ...archive.lessons.map((entry) => ({ entry, location: "lessons" })),
1169
+ ...archive.events.map((entry) => ({ entry, location: "events" })),
1170
+ ...archive.preferences.map((entry) => ({ entry, location: "preferences" })),
1171
+ ...archive.facets.map((entry) => ({ entry, location: "facets" })),
1172
+ ]
1173
+ .filter(({ entry }) => !scope?.userId || !entry.scope?.userId || entry.scope.userId === scope.userId)
1174
+ .filter(({ entry }) => !scope?.agentId || !entry.scope?.agentId || entry.scope.agentId === scope.agentId)
1175
+ .map(({ entry, location }) => ({
1176
+ id: entry.id,
1177
+ location,
1178
+ score: scoreEntry(entry, project || entry.project, contextTags, hl, sw),
1179
+ }))
1180
+ .filter((candidate) => candidate.score >= legacyThreshold)
1181
+ .sort((a, b) => b.score - a.score)
1182
+ .slice(0, maxItems);
1183
+ for (const candidate of legacyCandidates) {
1184
+ const result = await this.restoreArchivedEntry(candidate.id);
1185
+ if (result.ok && result.location)
1186
+ revived.push({ id: candidate.id, location: result.location });
1187
+ }
1188
+ if (revived.length >= maxItems)
1189
+ return revived;
1190
+ const semanticCandidates = archive.semantic
1191
+ .filter((entry) => !scope?.userId || !entry.scope?.userId || entry.scope.userId === scope.userId)
1192
+ .filter((entry) => !scope?.agentId || !entry.scope?.agentId || entry.scope.agentId === scope.agentId)
1193
+ .filter((entry) => !project || !entry.scope?.project || entry.scope.project === project)
1194
+ .map((entry) => ({ id: entry.id, location: "semantic", score: this.scoreV2SemanticMemory(entry, project, contextTags) }))
1195
+ .filter((candidate) => candidate.score >= v2Threshold);
1196
+ const proceduralCandidates = archive.procedural
1197
+ .filter((entry) => !scope?.userId || !entry.scope?.userId || entry.scope.userId === scope.userId)
1198
+ .filter((entry) => !scope?.agentId || !entry.scope?.agentId || entry.scope.agentId === scope.agentId)
1199
+ .filter((entry) => !project || !entry.scope?.project || entry.scope.project === project)
1200
+ .map((entry) => ({
1201
+ id: entry.id,
1202
+ location: "procedural",
1203
+ // Archived procedures are often older superseded versions, so revive scoring should not punish status as heavily.
1204
+ score: this.scoreProceduralMemory(entry, project, contextTags) + (entry.status === "superseded" || entry.status === "deprecated" ? 0.24 : 0),
1205
+ }))
1206
+ .filter((candidate) => candidate.score >= v2Threshold);
1207
+ const v2Candidates = [...semanticCandidates, ...proceduralCandidates]
1208
+ .sort((a, b) => b.score - a.score)
1209
+ .slice(0, Math.max(0, maxItems - revived.length));
1210
+ for (const candidate of v2Candidates) {
1211
+ const result = await this.restoreArchivedEntry(candidate.id);
1212
+ if (result.ok && result.location)
1213
+ revived.push({ id: candidate.id, location: result.location });
1214
+ }
1215
+ return revived;
1216
+ }
1217
+ async getAlignmentSnapshot() {
1218
+ const all = await this.buildRuntimeMemoryView();
1219
+ const memoryEntries = [...all.knowledge, ...all.lessons, ...all.events, ...all.preferences, ...all.facets];
1220
+ const sortByInfluence = (entries) => [...entries].sort((a, b) => {
1221
+ const aScore = (a.salience ?? a.importance) * ((a.accessCount ?? 0) + 1) + (a.retention === "core" ? 2 : 0);
1222
+ const bScore = (b.salience ?? b.importance) * ((b.accessCount ?? 0) + 1) + (b.retention === "core" ? 2 : 0);
1223
+ return bScore - aScore;
1224
+ });
1225
+ const identityCore = sortByInfluence(memoryEntries.filter((entry) => entry.stability !== "situational" && entry.retention === "core")).slice(0, 8);
1226
+ const keyEvents = sortByInfluence(memoryEntries.filter((entry) => entry.type === "event" || entry.retention === "key-event")).slice(0, 8);
1227
+ const behaviorDrivers = sortByInfluence(memoryEntries.filter((entry) => entry.type === "pattern" || entry.type === "preference" || entry.type === "lesson")).slice(0, 8);
1228
+ const currentState = sortByInfluence(memoryEntries.filter((entry) => entry.stability === "situational" || entry.stateData)).slice(0, 6);
1229
+ const relationshipEdges = sortByInfluence(memoryEntries)
1230
+ .flatMap((entry) => (entry.relations ?? []).map((relation) => ({
1231
+ fromId: entry.id,
1232
+ toId: relation.id,
1233
+ kind: relation.kind,
1234
+ weight: relation.weight,
1235
+ })))
1236
+ .sort((a, b) => b.weight - a.weight)
1237
+ .slice(0, 12);
1238
+ const conflicts = this.detectAlignmentConflicts(memoryEntries).slice(0, 8);
1239
+ return {
1240
+ identityCore,
1241
+ keyEvents,
1242
+ behaviorDrivers,
1243
+ currentState,
1244
+ relationshipEdges,
1245
+ conflicts,
1246
+ generatedAt: new Date().toISOString(),
1247
+ };
1248
+ }
1249
+ async forgetEntry(id) {
1250
+ const paths = [this.knowledgePath, this.lessonsPath, this.eventsPath, this.preferencesPath, this.facetsPath];
1251
+ for (const path of paths) {
1252
+ const entries = await loadEntries(path);
1253
+ const idx = entries.findIndex((e) => e.id === id);
1254
+ if (idx >= 0) {
1255
+ const forgotten = entries[idx];
1256
+ entries.splice(idx, 1);
1257
+ const hl = this.cfg.halfLife;
1258
+ const ew = this.cfg.evictionWeights;
1259
+ await saveEntries(path, entries, Infinity, (e) => utilityEntry(e, hl, ew));
1260
+ const archivePath = path.replace(`${this.cfg.memoryDir}/`, `${this.cfg.memoryDir}/_archive/`);
1261
+ const archived = await loadEntries(archivePath);
1262
+ if (archived.some((entry) => entry.id === id))
1263
+ await saveEntries(archivePath, archived.filter((entry) => entry.id !== id), Infinity, (e) => utilityEntry(e, hl, ew));
1264
+ const removedIds = new Set([id]);
1265
+ for (const v2Paths of [this.v2Paths, this.archiveV2Paths]) {
1266
+ const semantic = await loadV2Semantic(v2Paths);
1267
+ const retained = semantic.filter((entry) => {
1268
+ const matches = (entry.legacyIds?.includes(id) || (entry.name === forgotten.name && entry.summary === forgotten.summary && entry.scope?.project === forgotten.project));
1269
+ if (matches)
1270
+ removedIds.add(entry.id);
1271
+ return !matches;
1272
+ });
1273
+ if (retained.length !== semantic.length)
1274
+ await saveV2Semantic(v2Paths, retained);
1275
+ }
1276
+ for (const v2Paths of [this.v2Paths, this.archiveV2Paths]) {
1277
+ const links = await loadV2Links(v2Paths);
1278
+ if (links.some((link) => removedIds.has(link.fromId) || removedIds.has(link.toId))) {
1279
+ await saveV2Links(v2Paths, links.filter((link) => !removedIds.has(link.fromId) && !removedIds.has(link.toId)));
1280
+ }
1281
+ }
1282
+ const index = await loadEmbeddingIndex(this.cfg.memoryDir);
1283
+ if (index.records.some((record) => removedIds.has(record.memoryId))) {
1284
+ await saveEmbeddingIndex(this.cfg.memoryDir, { ...index, records: index.records.filter((record) => !removedIds.has(record.memoryId)) });
1285
+ }
1286
+ return true;
1287
+ }
1288
+ }
1289
+ const semantic = (await loadV2Semantic(this.v2Paths)).find(entry => entry.id === id);
1290
+ if (semantic) {
1291
+ const legacy = Object.values(await this.getAllEntries()).flat().find(entry => (semantic.legacyIds?.includes(entry.id) || (entry.name === semantic.name && entry.summary === semantic.summary && entry.project === semantic.scope?.project)));
1292
+ if (legacy)
1293
+ return this.forgetEntry(legacy.id);
1294
+ }
1295
+ let removed = false;
1296
+ for (const paths of [this.v2Paths, this.archiveV2Paths]) {
1297
+ const semantic = await loadV2Semantic(paths);
1298
+ const procedural = await loadV2Procedural(paths);
1299
+ if (semantic.some(entry => entry.id === id)) {
1300
+ await saveV2Semantic(paths, semantic.filter(entry => entry.id !== id));
1301
+ removed = true;
1302
+ }
1303
+ if (procedural.some(entry => entry.id === id)) {
1304
+ await saveV2Procedural(paths, procedural.filter(entry => entry.id !== id));
1305
+ removed = true;
1306
+ }
1307
+ const links = await loadV2Links(paths);
1308
+ await saveV2Links(paths, links.filter(link => link.fromId !== id && link.toId !== id));
1309
+ }
1310
+ if (removed) {
1311
+ const index = await loadEmbeddingIndex(this.cfg.memoryDir);
1312
+ await saveEmbeddingIndex(this.cfg.memoryDir, { ...index, records: index.records.filter(record => record.memoryId !== id) });
1313
+ }
1314
+ return removed;
1315
+ }
1316
+ async exportAll() {
1317
+ const [knowledge, lessons, events, preferences, facets, work, episodes, meta] = await Promise.all([
1318
+ loadEntries(this.knowledgePath),
1319
+ loadEntries(this.lessonsPath),
1320
+ loadEntries(this.eventsPath),
1321
+ loadEntries(this.preferencesPath),
1322
+ loadEntries(this.facetsPath),
1323
+ loadWork(this.workPath),
1324
+ loadEpisodes(this.episodesDir),
1325
+ loadMeta(this.metaPath),
1326
+ ]);
1327
+ return { knowledge, lessons, events, preferences, facets, work, episodes, meta };
1328
+ }
1329
+ async exportArchive() {
1330
+ const [knowledge, lessons, events, preferences, facets, work, semantic, procedural] = await Promise.all([
1331
+ loadEntries(this.archiveKnowledgePath),
1332
+ loadEntries(this.archiveLessonsPath),
1333
+ loadEntries(this.archiveEventsPath),
1334
+ loadEntries(this.archivePreferencesPath),
1335
+ loadEntries(this.archiveFacetsPath),
1336
+ loadWork(this.archiveWorkPath),
1337
+ loadV2Semantic(this.archiveV2Paths),
1338
+ loadV2Procedural(this.archiveV2Paths),
1339
+ ]);
1340
+ return { knowledge, lessons, events, preferences, facets, work, semantic, procedural };
1341
+ }
1342
+ async exportAllV2() {
1343
+ const [episodes, facets, semantic, procedural, links, state] = await Promise.all([
1344
+ loadV2Episodes(this.v2Paths),
1345
+ loadV2Facets(this.v2Paths),
1346
+ loadV2Semantic(this.v2Paths),
1347
+ loadV2Procedural(this.v2Paths),
1348
+ loadV2Links(this.v2Paths),
1349
+ loadV2State(this.v2Paths),
1350
+ ]);
1351
+ return { episodes, facets, semantic, procedural, links, state };
1352
+ }
1353
+ async rebuildV2Links() {
1354
+ const snapshot = await this.exportAllV2();
1355
+ const nextLinks = this.materializeV2Links(snapshot.semantic, snapshot.procedural, snapshot.links);
1356
+ await saveV2Links(this.v2Paths, nextLinks);
1357
+ return {
1358
+ total: nextLinks.length,
1359
+ auto: nextLinks.filter((link) => link.id.startsWith(NanoMemEngine.AUTO_V2_LINK_PREFIX)).length,
1360
+ };
1361
+ }
1362
+ async archiveStaleMemories(now = new Date().toISOString()) {
1363
+ const [knowledge, lessons, events, preferences, facets, work, archived, semantic, procedural, archivedSemantic, archivedProcedural] = await Promise.all([
1364
+ loadEntries(this.knowledgePath),
1365
+ loadEntries(this.lessonsPath),
1366
+ loadEntries(this.eventsPath),
1367
+ loadEntries(this.preferencesPath),
1368
+ loadEntries(this.facetsPath),
1369
+ loadWork(this.workPath),
1370
+ this.exportArchive(),
1371
+ loadV2Semantic(this.v2Paths),
1372
+ loadV2Procedural(this.v2Paths),
1373
+ loadV2Semantic(this.archiveV2Paths),
1374
+ loadV2Procedural(this.archiveV2Paths),
1375
+ ]);
1376
+ const knowledgeResult = this.partitionArchivedEntries(knowledge, now);
1377
+ const lessonsResult = this.partitionArchivedEntries(lessons, now);
1378
+ const eventsResult = this.partitionArchivedEntries(events, now);
1379
+ const preferencesResult = this.partitionArchivedEntries(preferences, now);
1380
+ const facetsResult = this.partitionArchivedEntries(facets, now);
1381
+ const workResult = this.partitionArchivedWork(work, now);
1382
+ const semanticResult = this.partitionArchivedSemantic(semantic, now);
1383
+ const proceduralResult = this.partitionArchivedProcedural(procedural, now);
1384
+ const hl = this.cfg.halfLife;
1385
+ const ew = this.cfg.evictionWeights;
1386
+ await Promise.all([
1387
+ saveEntries(this.knowledgePath, knowledgeResult.active, this.cfg.maxEntries.knowledge, (entry) => utilityEntry(entry, hl, ew)),
1388
+ saveEntries(this.lessonsPath, lessonsResult.active, this.cfg.maxEntries.lessons, (entry) => utilityEntry(entry, hl, ew)),
1389
+ saveEntries(this.eventsPath, eventsResult.active, this.cfg.maxEntries.events, (entry) => utilityEntry(entry, hl, ew)),
1390
+ saveEntries(this.preferencesPath, preferencesResult.active, this.cfg.maxEntries.preferences, (entry) => utilityEntry(entry, hl, ew)),
1391
+ saveEntries(this.facetsPath, facetsResult.active, this.cfg.maxEntries.facets, (entry) => utilityEntry(entry, hl, ew)),
1392
+ saveWork(this.workPath, workResult.active, this.cfg.maxEntries.work, (entry) => utilityWork(entry, this.cfg.halfLife, this.cfg.evictionWeights)),
1393
+ saveEntries(this.archiveKnowledgePath, this.mergeArchivedEntries(archived.knowledge, knowledgeResult.archived), Infinity, (entry) => utilityEntry(entry, hl, ew)),
1394
+ saveEntries(this.archiveLessonsPath, this.mergeArchivedEntries(archived.lessons, lessonsResult.archived), Infinity, (entry) => utilityEntry(entry, hl, ew)),
1395
+ saveEntries(this.archiveEventsPath, this.mergeArchivedEntries(archived.events, eventsResult.archived), Infinity, (entry) => utilityEntry(entry, hl, ew)),
1396
+ saveEntries(this.archivePreferencesPath, this.mergeArchivedEntries(archived.preferences, preferencesResult.archived), Infinity, (entry) => utilityEntry(entry, hl, ew)),
1397
+ saveEntries(this.archiveFacetsPath, this.mergeArchivedEntries(archived.facets, facetsResult.archived), Infinity, (entry) => utilityEntry(entry, hl, ew)),
1398
+ saveWork(this.archiveWorkPath, this.mergeArchivedWork(archived.work, workResult.archived), Infinity, (entry) => utilityWork(entry, this.cfg.halfLife, this.cfg.evictionWeights)),
1399
+ saveV2Semantic(this.v2Paths, semanticResult.active),
1400
+ saveV2Procedural(this.v2Paths, proceduralResult.active),
1401
+ saveV2Semantic(this.archiveV2Paths, this.mergeArchivedV2(archivedSemantic, semanticResult.archived)),
1402
+ saveV2Procedural(this.archiveV2Paths, this.mergeArchivedV2(archivedProcedural, proceduralResult.archived)),
1403
+ ]);
1404
+ const total = knowledgeResult.archived.length +
1405
+ lessonsResult.archived.length +
1406
+ eventsResult.archived.length +
1407
+ preferencesResult.archived.length +
1408
+ facetsResult.archived.length +
1409
+ workResult.archived.length +
1410
+ semanticResult.archived.length +
1411
+ proceduralResult.archived.length;
1412
+ return {
1413
+ knowledge: knowledgeResult.archived.length,
1414
+ lessons: lessonsResult.archived.length,
1415
+ events: eventsResult.archived.length,
1416
+ preferences: preferencesResult.archived.length,
1417
+ facets: facetsResult.archived.length,
1418
+ work: workResult.archived.length,
1419
+ semantic: semanticResult.archived.length,
1420
+ procedural: proceduralResult.archived.length,
1421
+ total,
1422
+ };
1423
+ }
1424
+ async restoreArchivedEntry(id) {
1425
+ const hl = this.cfg.halfLife;
1426
+ const ew = this.cfg.evictionWeights;
1427
+ const restoreLegacy = async (activePath, archivePath, location) => {
1428
+ const [active, archived] = await Promise.all([loadEntries(activePath), loadEntries(archivePath)]);
1429
+ const index = archived.findIndex((entry) => entry.id === id);
1430
+ if (index < 0)
1431
+ return { ok: false };
1432
+ const [entry] = archived.splice(index, 1);
1433
+ active.push({
1434
+ ...entry,
1435
+ archivedAt: undefined,
1436
+ archiveReason: undefined,
1437
+ revivedAt: new Date().toISOString(),
1438
+ });
1439
+ await Promise.all([
1440
+ saveEntries(activePath, active, Infinity, (candidate) => utilityEntry(candidate, hl, ew)),
1441
+ saveEntries(archivePath, archived, Infinity, (candidate) => utilityEntry(candidate, hl, ew)),
1442
+ ]);
1443
+ return { ok: true, location };
1444
+ };
1445
+ for (const [activePath, archivePath, location] of [
1446
+ [this.knowledgePath, this.archiveKnowledgePath, "knowledge"],
1447
+ [this.lessonsPath, this.archiveLessonsPath, "lessons"],
1448
+ [this.eventsPath, this.archiveEventsPath, "events"],
1449
+ [this.preferencesPath, this.archivePreferencesPath, "preferences"],
1450
+ [this.facetsPath, this.archiveFacetsPath, "facets"],
1451
+ ]) {
1452
+ const result = await restoreLegacy(activePath, archivePath, location);
1453
+ if (result.ok)
1454
+ return result;
1455
+ }
1456
+ {
1457
+ const [active, archived] = await Promise.all([loadWork(this.workPath), loadWork(this.archiveWorkPath)]);
1458
+ const index = archived.findIndex((entry) => entry.id === id);
1459
+ if (index >= 0) {
1460
+ const [entry] = archived.splice(index, 1);
1461
+ active.push({
1462
+ ...entry,
1463
+ archivedAt: undefined,
1464
+ archiveReason: undefined,
1465
+ revivedAt: new Date().toISOString(),
1466
+ });
1467
+ await Promise.all([
1468
+ saveWork(this.workPath, active, Infinity, (candidate) => utilityWork(candidate, this.cfg.halfLife, this.cfg.evictionWeights)),
1469
+ saveWork(this.archiveWorkPath, archived, Infinity, (candidate) => utilityWork(candidate, this.cfg.halfLife, this.cfg.evictionWeights)),
1470
+ ]);
1471
+ return { ok: true, location: "work" };
1472
+ }
1473
+ }
1474
+ {
1475
+ const [active, archived] = await Promise.all([loadV2Semantic(this.v2Paths), loadV2Semantic(this.archiveV2Paths)]);
1476
+ const index = archived.findIndex((entry) => entry.id === id);
1477
+ if (index >= 0) {
1478
+ const [entry] = archived.splice(index, 1);
1479
+ active.push({
1480
+ ...entry,
1481
+ archivedAt: undefined,
1482
+ archiveReason: undefined,
1483
+ revivedAt: new Date().toISOString(),
1484
+ });
1485
+ await Promise.all([saveV2Semantic(this.v2Paths, active), saveV2Semantic(this.archiveV2Paths, archived)]);
1486
+ await this.rebuildV2Links();
1487
+ return { ok: true, location: "semantic" };
1488
+ }
1489
+ }
1490
+ {
1491
+ const [active, archived] = await Promise.all([loadV2Procedural(this.v2Paths), loadV2Procedural(this.archiveV2Paths)]);
1492
+ const index = archived.findIndex((entry) => entry.id === id);
1493
+ if (index >= 0) {
1494
+ const [entry] = archived.splice(index, 1);
1495
+ active.push({
1496
+ ...entry,
1497
+ archivedAt: undefined,
1498
+ archiveReason: undefined,
1499
+ revivedAt: new Date().toISOString(),
1500
+ });
1501
+ await Promise.all([saveV2Procedural(this.v2Paths, active), saveV2Procedural(this.archiveV2Paths, archived)]);
1502
+ await this.rebuildV2Links();
1503
+ return { ok: true, location: "procedural" };
1504
+ }
1505
+ }
1506
+ return { ok: false };
1507
+ }
1508
+ async inspectV2Memory(project = "", scope) {
1509
+ const snapshot = await this.exportAllV2();
1510
+ const episodes = this.filterAndCleanV2Episodes(snapshot.episodes, project, scope);
1511
+ const facets = this.filterAndCleanV2Facets(snapshot.facets, project, scope);
1512
+ const semantic = this.filterAndCleanV2Semantic(snapshot.semantic, project, scope);
1513
+ const allProcedural = snapshot.procedural.filter((entry) => {
1514
+ if (scope?.userId && entry.scope?.userId && entry.scope.userId !== scope.userId)
1515
+ return false;
1516
+ if (scope?.agentId && entry.scope?.agentId && entry.scope.agentId !== scope.agentId)
1517
+ return false;
1518
+ if (project && entry.scope?.project && entry.scope.project !== project)
1519
+ return false;
1520
+ return true;
1521
+ });
1522
+ const activeProcedural = allProcedural.filter((entry) => entry.status !== "deprecated" && entry.status !== "superseded");
1523
+ const procedureChains = this.buildProceduralChains(allProcedural);
1524
+ const proceduralConflicts = this.detectProceduralConflicts(activeProcedural);
1525
+ const semanticConflicts = this.detectSemanticConflicts(semantic);
1526
+ return {
1527
+ counts: {
1528
+ episodes: episodes.length,
1529
+ facets: facets.length,
1530
+ semantic: semantic.length,
1531
+ procedural: allProcedural.length,
1532
+ activeProcedural: activeProcedural.length,
1533
+ supersededProcedural: allProcedural.filter((entry) => entry.status === "superseded").length,
1534
+ semanticConflicts: semanticConflicts.length,
1535
+ proceduralConflicts: proceduralConflicts.length,
1536
+ procedureChains: procedureChains.length,
1537
+ },
1538
+ procedureChains,
1539
+ proceduralConflicts,
1540
+ semanticConflicts,
1541
+ };
1542
+ }
1543
+ // ─── Full Insights Report ────────────────────────────────────────
1544
+ async generateFullInsights() {
1545
+ const all = await this.buildRuntimeMemoryView();
1546
+ return buildFullInsightsReport(all, this.llmFn, this.cfg.locale);
1547
+ }
1548
+ /**
1549
+ * Generate enhanced insights report (includes human-readable insights + developer persona + root cause analysis)
1550
+ */
1551
+ async generateEnhancedInsights() {
1552
+ const all = await this.buildRuntimeMemoryView();
1553
+ // Generate base report and human-readable insights in parallel
1554
+ const [baseReport, humanData] = await Promise.all([
1555
+ buildFullInsightsReport(all, this.llmFn, this.cfg.locale),
1556
+ generateHumanInsights(all, this.llmFn, this.cfg.locale),
1557
+ ]);
1558
+ return {
1559
+ report: baseReport,
1560
+ persona: humanData.persona,
1561
+ humanInsights: humanData.humanInsights,
1562
+ rootCauses: humanData.rootCauses,
1563
+ };
1564
+ }
1565
+ // ─── Insights Generation ─────────────────────────────────────
1566
+ async generateInsights() {
1567
+ const all = await this.buildRuntimeMemoryView();
1568
+ return generateInsightsReport(all, this.cfg.locale, this.llmFn);
1569
+ }
1570
+ // ─── Private Helpers ─────────────────────────────────────────
1571
+ getMemoryPathConfigs() {
1572
+ return [
1573
+ { path: this.knowledgePath, max: this.cfg.maxEntries.knowledge },
1574
+ { path: this.lessonsPath, max: this.cfg.maxEntries.lessons },
1575
+ { path: this.eventsPath, max: this.cfg.maxEntries.events },
1576
+ { path: this.preferencesPath, max: this.cfg.maxEntries.preferences },
1577
+ { path: this.facetsPath, max: this.cfg.maxEntries.facets },
1578
+ ];
1579
+ }
1580
+ async findEntryLocation(id) {
1581
+ for (const { path, max } of this.getMemoryPathConfigs()) {
1582
+ const entries = await loadEntries(path);
1583
+ const entry = entries.find((candidate) => candidate.id === id);
1584
+ if (entry)
1585
+ return { entry, entries, path, max };
1586
+ }
1587
+ return null;
1588
+ }
1589
+ async persistEntries(path, entries, max) {
1590
+ const hl = this.cfg.halfLife;
1591
+ const ew = this.cfg.evictionWeights;
1592
+ await saveEntries(path, entries, max, (entry) => utilityEntry(entry, hl, ew));
1593
+ }
1594
+ filterAndCleanEntries(entries, scope) {
1595
+ return filterByScope(evictExpiredEntries(entries).filter((entry) => !entry.archivedAt), scope);
1596
+ }
1597
+ filterAndCleanWork(entries, scope) {
1598
+ return filterByScope(evictExpiredWork(entries).filter((entry) => !entry.archivedAt), scope);
1599
+ }
1600
+ filterAndCleanProcedural(entries, project, scope) {
1601
+ return entries.filter((entry) => {
1602
+ if (scope?.userId && entry.scope?.userId && entry.scope.userId !== scope.userId)
1603
+ return false;
1604
+ if (scope?.agentId && entry.scope?.agentId && entry.scope.agentId !== scope.agentId)
1605
+ return false;
1606
+ if (project && entry.scope?.project && entry.scope.project !== project)
1607
+ return false;
1608
+ return !entry.archivedAt && entry.status !== "deprecated" && entry.status !== "superseded";
1609
+ });
1610
+ }
1611
+ filterAndCleanV2Episodes(entries, project, scope) {
1612
+ return entries.filter((entry) => {
1613
+ if (scope?.userId && entry.scope?.userId && entry.scope.userId !== scope.userId)
1614
+ return false;
1615
+ if (scope?.agentId && entry.scope?.agentId && entry.scope.agentId !== scope.agentId)
1616
+ return false;
1617
+ if (project && entry.scope?.project && entry.scope.project !== project)
1618
+ return false;
1619
+ return !entry.archivedAt;
1620
+ });
1621
+ }
1622
+ filterAndCleanV2Facets(entries, project, scope) {
1623
+ return entries.filter((entry) => {
1624
+ if (scope?.userId && entry.scope?.userId && entry.scope.userId !== scope.userId)
1625
+ return false;
1626
+ if (scope?.agentId && entry.scope?.agentId && entry.scope.agentId !== scope.agentId)
1627
+ return false;
1628
+ if (project && entry.scope?.project && entry.scope.project !== project)
1629
+ return false;
1630
+ return !entry.archivedAt;
1631
+ });
1632
+ }
1633
+ filterAndCleanV2Semantic(entries, project, scope) {
1634
+ return entries.filter((entry) => {
1635
+ if (scope?.userId && entry.scope?.userId && entry.scope.userId !== scope.userId)
1636
+ return false;
1637
+ if (scope?.agentId && entry.scope?.agentId && entry.scope.agentId !== scope.agentId)
1638
+ return false;
1639
+ if (project && entry.scope?.project && entry.scope.project !== project)
1640
+ return false;
1641
+ return !entry.archivedAt && !entry.supersededById;
1642
+ });
1643
+ }
1644
+ mergeArchivedEntries = mergeArchivedEntriesFn;
1645
+ mergeArchivedWork = mergeArchivedWorkFn;
1646
+ mergeArchivedV2 = mergeArchivedV2Fn;
1647
+ partitionArchivedEntries(entries, now) { return partitionArchivedEntriesFn(entries, now, this.cfg.forgetting); }
1648
+ partitionArchivedWork(entries, now) { return partitionArchivedWorkFn(entries, now, this.cfg.forgetting); }
1649
+ partitionArchivedSemantic(entries, now) { return partitionArchivedSemanticFn(entries, now, this.cfg.forgetting); }
1650
+ partitionArchivedProcedural(entries, now) { return partitionArchivedProceduralFn(entries, now, this.cfg.forgetting); }
1651
+ getLegacyArchiveReason(entry) {
1652
+ const f = this.cfg.forgetting;
1653
+ if (entry.archivedAt)
1654
+ return entry.archiveReason ?? "pre-archived";
1655
+ if (entry.revivedAt && daysSince(entry.revivedAt) <= f.reviveCooldownDays)
1656
+ return undefined;
1657
+ const anchor = entry.lastAccessed ?? entry.eventTime ?? entry.created;
1658
+ const ageDays = daysSince(anchor);
1659
+ const lowSignal = (entry.accessCount ?? 0) <= 1 && entry.importance <= 6 && (entry.salience ?? entry.importance) <= 6;
1660
+ if (entry.retention === "ambient" && lowSignal && ageDays > f.ambientTtlDays)
1661
+ return "stale-ambient-memory";
1662
+ return undefined;
1663
+ }
1664
+ inferSemanticRetention = inferSemanticRetentionFn;
1665
+ inferSemanticStability = inferSemanticStabilityFn;
1666
+ inferSemanticImportance = inferSemanticImportanceFn;
1667
+ mapExtractedItemToSemanticType = mapExtractedItemToSemanticTypeFn;
1668
+ upsertSemanticFromExtractedItem(semantic, item, project) {
1669
+ upsertSemanticFromExtractedItemFn(semantic, item, project, this.cfg.defaultScope);
1670
+ }
1671
+ semanticKindToLegacyType = semanticKindToLegacyTypeFn;
1672
+ semanticToRuntimeEntry = semanticToRuntimeEntryFn;
1673
+ proceduralToRuntimeEntry = proceduralToRuntimeEntryFn;
1674
+ async buildRuntimeMemoryView() {
1675
+ const [legacy, work, episodes, meta, semantic, procedural] = await Promise.all([
1676
+ this.exportAll(),
1677
+ this.getAllWork(),
1678
+ this.getAllEpisodes(),
1679
+ loadMeta(this.metaPath),
1680
+ loadV2Semantic(this.v2Paths),
1681
+ loadV2Procedural(this.v2Paths),
1682
+ ]);
1683
+ const cleanSemantic = semantic.filter((entry) => !entry.archivedAt && !entry.supersededById);
1684
+ const cleanProcedural = procedural.filter((entry) => !entry.archivedAt && entry.status !== "deprecated");
1685
+ const semanticEntries = cleanSemantic.map((entry) => this.semanticToRuntimeEntry(entry));
1686
+ const proceduralEntries = cleanProcedural.map((entry) => this.proceduralToRuntimeEntry(entry));
1687
+ const v2SignalCount = semanticEntries.length + proceduralEntries.length;
1688
+ const useLegacyFallback = v2SignalCount === 0;
1689
+ const byType = (entries, type) => entries.filter((entry) => entry.type === type);
1690
+ const runtimeEntries = [...semanticEntries, ...proceduralEntries];
1691
+ return {
1692
+ knowledge: useLegacyFallback ? legacy.knowledge : byType(runtimeEntries, "fact").filter((entry) => entry.type === "fact"),
1693
+ lessons: useLegacyFallback ? legacy.lessons : byType(runtimeEntries, "lesson"),
1694
+ events: useLegacyFallback ? legacy.events : byType(runtimeEntries, "event"),
1695
+ preferences: useLegacyFallback ? legacy.preferences : byType(runtimeEntries, "preference"),
1696
+ facets: legacy.facets,
1697
+ work,
1698
+ episodes,
1699
+ meta,
1700
+ v2SearchEntries: runtimeEntries,
1701
+ };
1702
+ }
1703
+ getWorkArchiveReason(entry) {
1704
+ const f = this.cfg.forgetting;
1705
+ if (entry.archivedAt)
1706
+ return entry.archiveReason ?? "pre-archived";
1707
+ if (entry.revivedAt && daysSince(entry.revivedAt) <= f.reviveCooldownDays)
1708
+ return undefined;
1709
+ const anchor = entry.lastAccessed ?? entry.eventTime ?? entry.created;
1710
+ const ageDays = daysSince(anchor);
1711
+ if ((entry.accessCount ?? 0) <= 1 && entry.importance <= 6 && ageDays > f.workTtlDays)
1712
+ return "stale-work-memory";
1713
+ return undefined;
1714
+ }
1715
+ getSemanticArchiveReason(entry) {
1716
+ const f = this.cfg.forgetting;
1717
+ if (entry.archivedAt)
1718
+ return entry.archiveReason ?? "pre-archived";
1719
+ if (entry.revivedAt && daysSince(entry.revivedAt) <= f.reviveCooldownDays)
1720
+ return undefined;
1721
+ const anchor = entry.lastAccessedAt ?? entry.updatedAt ?? entry.createdAt;
1722
+ const ageDays = daysSince(anchor);
1723
+ if (entry.supersededById && ageDays > f.ambientTtlDays)
1724
+ return "superseded-semantic-memory";
1725
+ const lowSignal = (entry.accessCount ?? 0) <= 1 && entry.importance <= 6 && entry.confidence < 0.85;
1726
+ if (entry.retention === "ambient" && lowSignal && ageDays > f.ambientTtlDays * 2)
1727
+ return "stale-semantic-memory";
1728
+ return undefined;
1729
+ }
1730
+ getProceduralArchiveReason(entry) {
1731
+ const f = this.cfg.forgetting;
1732
+ if (entry.archivedAt)
1733
+ return entry.archiveReason ?? "pre-archived";
1734
+ if (entry.revivedAt && daysSince(entry.revivedAt) <= f.reviveCooldownDays)
1735
+ return undefined;
1736
+ const anchor = entry.lastAccessedAt ?? entry.updatedAt ?? entry.createdAt;
1737
+ const ageDays = daysSince(anchor);
1738
+ if ((entry.status === "superseded" || entry.status === "deprecated") && ageDays > f.ambientTtlDays)
1739
+ return "stale-procedure-version";
1740
+ if (entry.status === "draft" && (entry.accessCount ?? 0) <= 1 && entry.importance <= 6 && ageDays > f.ambientTtlDays * 2)
1741
+ return "abandoned-draft-procedure";
1742
+ return undefined;
1743
+ }
1744
+ buildProceduralChains = buildProceduralChainsFn;
1745
+ materializeV2Links(semantic, procedural, existingLinks, now = new Date().toISOString()) {
1746
+ return materializeV2LinksFn(semantic, procedural, existingLinks, AUTO_V2_LINK_PREFIX, now);
1747
+ }
1748
+ detectProceduralConflicts = detectProceduralConflictsFn;
1749
+ detectSemanticConflicts = detectSemanticConflictsFn;
1750
+ buildEmbeddingSourceItems(episodes, facets, semantic, procedural) {
1751
+ const episodeItems = episodes.map((entry) => ({
1752
+ memoryId: entry.id,
1753
+ memoryKind: "episode",
1754
+ text: [entry.title ?? "", entry.summary, entry.userGoal ?? "", entry.outcome ?? ""].filter(Boolean).join("\n"),
1755
+ }));
1756
+ const facetItems = facets.map((entry) => ({
1757
+ memoryId: entry.id,
1758
+ memoryKind: "facet",
1759
+ text: [entry.searchText, entry.summary ?? "", entry.anchorText ?? "", entry.detail ?? ""].filter(Boolean).join("\n"),
1760
+ }));
1761
+ const semanticItems = semantic.map((entry) => ({
1762
+ memoryId: entry.id,
1763
+ memoryKind: "semantic",
1764
+ text: [entry.name, entry.summary, entry.detail ?? ""].filter(Boolean).join("\n"),
1765
+ }));
1766
+ const proceduralItems = procedural.map((entry) => ({
1767
+ memoryId: entry.id,
1768
+ memoryKind: "procedural",
1769
+ text: [
1770
+ entry.name,
1771
+ entry.searchText,
1772
+ entry.summary,
1773
+ entry.contextText ?? "",
1774
+ entry.boundaries ?? "",
1775
+ entry.steps.map((step) => step.text).join("\n"),
1776
+ ]
1777
+ .filter(Boolean)
1778
+ .join("\n"),
1779
+ }));
1780
+ return [...episodeItems, ...facetItems, ...semanticItems, ...proceduralItems];
1781
+ }
1782
+ async querySemanticCandidates(queryText, project, scope, episodes, facets, semantic, procedural) {
1783
+ if (!this.embeddingFn || !queryText.trim())
1784
+ return [];
1785
+ const items = this.buildEmbeddingSourceItems(episodes, facets, semantic, procedural);
1786
+ if (!items.length)
1787
+ return [];
1788
+ await syncEmbeddingIndex(this.cfg.memoryDir, this.cfg.embeddings.model, items, this.embeddingFn);
1789
+ const matches = await queryEmbeddingIndex(this.cfg.memoryDir, this.cfg.embeddings.model, queryText, this.embeddingFn, 12);
1790
+ const visibleIds = new Set([
1791
+ ...episodes.map((entry) => entry.id),
1792
+ ...facets.map((entry) => entry.id),
1793
+ ...semantic.map((entry) => entry.id),
1794
+ ...procedural.map((entry) => entry.id),
1795
+ ]);
1796
+ return matches
1797
+ .filter((match) => visibleIds.has(match.memoryId))
1798
+ .map((match) => ({ memoryId: match.memoryId, memoryKind: match.memoryKind, score: match.score }))
1799
+ .filter((match) => match.score >= 0.18);
1800
+ }
1801
+ currentStructuralAnchor = currentStructuralAnchor;
1802
+ computeStructuralBoost = computeStructuralBoost;
1803
+ scoreEpisodeMemory(entry, project, contextTags) {
1804
+ return scoreEpisodeMemoryFn(entry, project, contextTags, this.cfg.structuralWeight);
1805
+ }
1806
+ scoreEpisodeFacet(entry, project, contextTags) {
1807
+ return scoreEpisodeFacetFn(entry, project, contextTags, this.cfg.structuralWeight);
1808
+ }
1809
+ scoreV2SemanticMemory(entry, project, contextTags) {
1810
+ return scoreV2SemanticMemoryFn(entry, project, contextTags, this.cfg.structuralWeight);
1811
+ }
1812
+ scoreProceduralMemory(entry, project, contextTags) {
1813
+ return scoreProceduralMemoryFn(entry, project, contextTags, this.cfg.structuralWeight);
1814
+ }
1815
+ async reinforceEntries(recalled, all, savePath) {
1816
+ const ids = new Set(recalled.map((e) => e.id));
1817
+ const now = new Date().toISOString();
1818
+ for (const entry of all) {
1819
+ if (ids.has(entry.id)) {
1820
+ entry.accessCount = (entry.accessCount ?? 0) + 1;
1821
+ entry.lastAccessed = now;
1822
+ entry.strength = (entry.strength || 30) * this.cfg.strengthGrowthFactor;
1823
+ }
1824
+ }
1825
+ reinforceRelations(recalled, all);
1826
+ const pathConfig = this.getMemoryPathConfigs().find((config) => config.path === savePath);
1827
+ await this.persistEntries(savePath, all, pathConfig?.max ?? this.cfg.maxEntries.knowledge);
1828
+ }
1829
+ async reinforceProcedural(recalled, all) {
1830
+ if (!recalled.length)
1831
+ return;
1832
+ const ids = new Set(recalled.map((entry) => entry.id));
1833
+ const now = new Date().toISOString();
1834
+ for (const entry of all) {
1835
+ if (!ids.has(entry.id))
1836
+ continue;
1837
+ entry.accessCount = (entry.accessCount ?? 0) + 1;
1838
+ entry.lastAccessedAt = now;
1839
+ entry.updatedAt = entry.updatedAt || now;
1840
+ }
1841
+ await saveV2Procedural(this.v2Paths, all);
1842
+ }
1843
+ async reinforceEpisodeMemories(recalled, all) {
1844
+ if (!recalled.length)
1845
+ return;
1846
+ const ids = new Set(recalled.map((entry) => entry.id));
1847
+ const now = new Date().toISOString();
1848
+ for (const entry of all) {
1849
+ if (!ids.has(entry.id))
1850
+ continue;
1851
+ entry.accessCount = (entry.accessCount ?? 0) + 1;
1852
+ entry.lastAccessedAt = now;
1853
+ entry.updatedAt = entry.updatedAt || now;
1854
+ }
1855
+ await saveV2Episodes(this.v2Paths, all);
1856
+ }
1857
+ async reinforceEpisodeFacets(recalled, all) {
1858
+ if (!recalled.length)
1859
+ return;
1860
+ const ids = new Set(recalled.map((entry) => entry.id));
1861
+ const now = new Date().toISOString();
1862
+ for (const entry of all) {
1863
+ if (!ids.has(entry.id))
1864
+ continue;
1865
+ entry.accessCount = (entry.accessCount ?? 0) + 1;
1866
+ entry.lastAccessedAt = now;
1867
+ entry.updatedAt = entry.updatedAt || now;
1868
+ }
1869
+ await saveV2Facets(this.v2Paths, all);
1870
+ }
1871
+ async reinforceV2SemanticMemories(recalled, all) {
1872
+ if (!recalled.length)
1873
+ return;
1874
+ const ids = new Set(recalled.map((entry) => entry.id));
1875
+ const now = new Date().toISOString();
1876
+ for (const entry of all) {
1877
+ if (!ids.has(entry.id))
1878
+ continue;
1879
+ entry.accessCount = (entry.accessCount ?? 0) + 1;
1880
+ entry.lastAccessedAt = now;
1881
+ entry.updatedAt = entry.updatedAt || now;
1882
+ }
1883
+ await saveV2Semantic(this.v2Paths, all);
1884
+ }
1885
+ async reconsolidateV2AfterRecall(episodes, facets, procedural, recalledEpisodes, recalledFacets, recalledProcedural) {
1886
+ const reconsolidated = reconsolidateV2Memories(episodes, facets, procedural, recalledEpisodes.map((entry) => entry.id), recalledFacets.map((entry) => entry.id), recalledProcedural.map((entry) => entry.id));
1887
+ await Promise.all([
1888
+ saveV2Episodes(this.v2Paths, reconsolidated.episodes),
1889
+ saveV2Facets(this.v2Paths, reconsolidated.facets),
1890
+ saveV2Procedural(this.v2Paths, reconsolidated.procedural),
1891
+ saveV2Meta(this.v2Paths, {
1892
+ ...(await loadV2Meta(this.v2Paths)),
1893
+ lastReconsolidationAt: new Date().toISOString(),
1894
+ }),
1895
+ ]);
1896
+ await this.rebuildV2Links();
1897
+ }
1898
+ detectAlignmentConflicts = detectAlignmentConflictsFn;
1899
+ suggestConflictAction = suggestConflictActionFn;
1900
+ explainConflictAction(a, b, action) {
1901
+ switch (action) {
1902
+ case "mark-situational":
1903
+ return "One side looks more temporary than the other, so this conflict likely comes from short-term context rather than true identity drift.";
1904
+ case "forget":
1905
+ return "One side has weak signal compared with the conflict risk, so forgetting the weaker memory is safer than keeping both.";
1906
+ case "demote":
1907
+ return "At least one side is recent enough that it may be an overfit; demoting it reduces the chance of personality drift.";
1908
+ default:
1909
+ return "Both memories may describe the same preference from different angles, so a merged memory is likely safer than keeping them separate.";
1910
+ }
1911
+ }
1912
+ async reinforceWork(recalled, all) {
1913
+ const ids = new Set(recalled.map((w) => w.id));
1914
+ const now = new Date().toISOString();
1915
+ for (const w of all) {
1916
+ if (ids.has(w.id)) {
1917
+ w.accessCount = (w.accessCount ?? 0) + 1;
1918
+ w.lastAccessed = now;
1919
+ w.strength = (w.strength || 45) * this.cfg.strengthGrowthFactor;
1920
+ }
1921
+ }
1922
+ await saveWork(this.workPath, all, this.cfg.maxEntries.work, (w) => utilityWork(w, this.cfg.halfLife, this.cfg.evictionWeights));
1923
+ }
1924
+ async reconsolidateIfNeeded(recalled, contextTags, _allEntries) {
1925
+ if (!this.llmFn)
1926
+ return;
1927
+ const p = PROMPTS[this.cfg.locale];
1928
+ for (const entry of recalled) {
1929
+ const overlap = tagOverlap(entry.tags, contextTags);
1930
+ if (overlap >= 0.3)
1931
+ continue;
1932
+ try {
1933
+ const memText = entry.detail || entry.summary || entry.content || "";
1934
+ const updated = await this.llmFn(p.reconsolidationSystem, `Original memory: ${memText}\n\nCurrent context tags: ${contextTags.join(", ")}`);
1935
+ if (updated && updated.length > 10) {
1936
+ const trimmed = updated.trim();
1937
+ entry.detail = trimmed;
1938
+ entry.content = trimmed;
1939
+ entry.summary = trimmed.length <= 150 ? trimmed : `${trimmed.slice(0, 147)}...`;
1940
+ entry.tags = extractTags(trimmed);
1941
+ }
1942
+ }
1943
+ catch {
1944
+ /* graceful degradation */
1945
+ }
1946
+ }
1947
+ }
1948
+ buildProgressiveInjectionText = buildProgressiveInjectionText;
1949
+ isConversationPreference = isConversationPreferenceFn;
1950
+ selectConversationPreferences = selectConversationPreferencesFn;
1951
+ rankConversationPreference = rankConversationPreferenceFn;
1952
+ mergeUniqueEntries(preferred, fallback) {
1953
+ const merged = [];
1954
+ const seen = new Set();
1955
+ for (const entry of [...preferred, ...fallback]) {
1956
+ if (seen.has(entry.id))
1957
+ continue;
1958
+ seen.add(entry.id);
1959
+ merged.push(entry);
1960
+ }
1961
+ return merged;
1962
+ }
1963
+ }
1964
+ //# sourceMappingURL=engine.js.map