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
@@ -0,0 +1,311 @@
1
+ /**
2
+ * [WHO]: Provides AUTO_V2_LINK_PREFIX, buildProceduralChains, materializeV2Links, detectProceduralConflicts, detectSemanticConflicts, detectAlignmentConflicts, suggestConflictAction, explainConflictAction
3
+ * [FROM]: Depends on ./scoring.js for daysSince, extractTags, tagOverlap; ./types.js, ./types-v2.js
4
+ * [TO]: Consumed by engine.ts (rebuildV2Links, inspectV2Memory, getAlignmentSnapshot)
5
+ * [HERE]: packages/mem-core/src/engine-links.ts - V2 link materialization, conflict detection, procedural chain building
6
+ */
7
+
8
+ import { daysSince, extractTags, tagOverlap } from "./scoring.js";
9
+ import type { MemoryEntry } from "./types.js";
10
+ import type { MemoryLink, ProceduralMemory, SemanticMemory } from "./types-v2.js";
11
+
12
+ export const AUTO_V2_LINK_PREFIX = "auto:v2:";
13
+
14
+ // ── Procedural chains ──────────────────────────────────────
15
+
16
+ export function buildProceduralChains(entries: ProceduralMemory[]): Array<{
17
+ rootId: string;
18
+ name: string;
19
+ status: ProceduralMemory["status"];
20
+ versionDepth: number;
21
+ ids: string[];
22
+ }> {
23
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
24
+ const roots = entries.filter((entry) => !entry.supersededById || !byId.has(entry.supersededById));
25
+ return roots
26
+ .map((root) => {
27
+ const ids = new Set<string>();
28
+ const stack = [root.id];
29
+ while (stack.length) {
30
+ const currentId = stack.pop();
31
+ if (!currentId || ids.has(currentId)) continue;
32
+ ids.add(currentId);
33
+ for (const entry of entries) {
34
+ if (entry.supersededById === currentId) stack.push(entry.id);
35
+ if (entry.supersedesIds?.includes(currentId)) stack.push(entry.id);
36
+ }
37
+ }
38
+ const chainEntries = [...ids].map((id) => byId.get(id)).filter((entry): entry is ProceduralMemory => Boolean(entry));
39
+ return {
40
+ rootId: root.id,
41
+ name: root.name,
42
+ status: root.status,
43
+ versionDepth: chainEntries.length,
44
+ ids: chainEntries
45
+ .sort((a, b) => {
46
+ const aVersion = a.version ?? 0;
47
+ const bVersion = b.version ?? 0;
48
+ if (aVersion !== bVersion) return bVersion - aVersion;
49
+ return a.updatedAt.localeCompare(b.updatedAt);
50
+ })
51
+ .map((entry) => entry.id),
52
+ };
53
+ })
54
+ .filter((chain) => chain.versionDepth > 1)
55
+ .sort((a, b) => b.versionDepth - a.versionDepth || a.name.localeCompare(b.name));
56
+ }
57
+
58
+ // ── V2 link materialization ────────────────────────────────
59
+
60
+ export function materializeV2Links(
61
+ semantic: SemanticMemory[],
62
+ procedural: ProceduralMemory[],
63
+ existingLinks: MemoryLink[],
64
+ autoLinkPrefix: string,
65
+ now = new Date().toISOString(),
66
+ ): MemoryLink[] {
67
+ const manualLinks = existingLinks.filter((link) => !link.id.startsWith(autoLinkPrefix));
68
+ const autoLinks = new Map<string, MemoryLink>();
69
+ const proceduralConflicts = detectProceduralConflicts(
70
+ procedural.filter((entry) => entry.status !== "deprecated" && entry.status !== "superseded"),
71
+ );
72
+ const semanticConflicts = detectSemanticConflicts(semantic);
73
+
74
+ const addAutoLink = (
75
+ fromId: string,
76
+ toId: string,
77
+ type: MemoryLink["type"],
78
+ weight: number,
79
+ evidence: MemoryLink["evidence"] = [],
80
+ ): void => {
81
+ if (!fromId || !toId || fromId === toId) return;
82
+ const directional = type === "supersedes";
83
+ const [normalizedFrom, normalizedTo] = directional || fromId < toId ? [fromId, toId] : [toId, fromId];
84
+ const id = `${autoLinkPrefix}${type}:${normalizedFrom}->${normalizedTo}`;
85
+ autoLinks.set(id, {
86
+ id,
87
+ fromId: normalizedFrom,
88
+ toId: normalizedTo,
89
+ type,
90
+ weight,
91
+ explicit: false,
92
+ createdAt: autoLinks.get(id)?.createdAt ?? now,
93
+ updatedAt: now,
94
+ evidence,
95
+ });
96
+ };
97
+
98
+ for (const entry of semantic) {
99
+ for (const supersededId of entry.supersedesIds ?? []) {
100
+ addAutoLink(entry.id, supersededId, "supersedes", 0.92);
101
+ }
102
+ if (entry.supersededById) {
103
+ addAutoLink(entry.supersededById, entry.id, "supersedes", 0.92);
104
+ }
105
+ }
106
+
107
+ for (const entry of procedural) {
108
+ for (const supersededId of entry.supersedesIds ?? []) {
109
+ addAutoLink(entry.id, supersededId, "supersedes", 0.94);
110
+ }
111
+ if (entry.supersededById) {
112
+ addAutoLink(entry.supersededById, entry.id, "supersedes", 0.94);
113
+ }
114
+ }
115
+
116
+ for (const conflict of semanticConflicts) {
117
+ addAutoLink(conflict.aId, conflict.bId, "conflicts-with", 0.88);
118
+ }
119
+
120
+ for (const conflict of proceduralConflicts) {
121
+ addAutoLink(conflict.aId, conflict.bId, "conflicts-with", Math.max(0.72, conflict.score));
122
+ }
123
+
124
+ return [...manualLinks, ...autoLinks.values()].sort((a, b) => a.id.localeCompare(b.id));
125
+ }
126
+
127
+ // ── Conflict detection ─────────────────────────────────────
128
+
129
+ export function detectProceduralConflicts(entries: ProceduralMemory[]): Array<{
130
+ aId: string;
131
+ bId: string;
132
+ aName: string;
133
+ bName: string;
134
+ score: number;
135
+ reason: string;
136
+ }> {
137
+ const conflicts: Array<{
138
+ aId: string;
139
+ bId: string;
140
+ aName: string;
141
+ bName: string;
142
+ score: number;
143
+ reason: string;
144
+ }> = [];
145
+ for (let i = 0; i < entries.length; i++) {
146
+ for (let j = i + 1; j < entries.length; j++) {
147
+ const a = entries[i];
148
+ const b = entries[j];
149
+ if (!a || !b || a.id === b.id) continue;
150
+ const tagScore = tagOverlap(a.tags ?? [], b.tags ?? []);
151
+ const lexicalScore = tagOverlap(
152
+ extractTags(`${a.name} ${a.searchText} ${a.summary}`),
153
+ extractTags(`${b.name} ${b.searchText} ${b.summary}`),
154
+ );
155
+ const similarity = Math.max(tagScore, lexicalScore);
156
+ const sameIntent =
157
+ a.searchText.trim().toLowerCase() === b.searchText.trim().toLowerCase() ||
158
+ a.name.trim().toLowerCase() === b.name.trim().toLowerCase();
159
+ const boundariesDiffer =
160
+ (a.boundaries ?? "").trim().toLowerCase() !== (b.boundaries ?? "").trim().toLowerCase() ||
161
+ (a.contextText ?? "").trim().toLowerCase() !== (b.contextText ?? "").trim().toLowerCase();
162
+ if ((similarity >= 0.72 || sameIntent) && boundariesDiffer) {
163
+ conflicts.push({
164
+ aId: a.id,
165
+ bId: b.id,
166
+ aName: a.name,
167
+ bName: b.name,
168
+ score: Number(similarity.toFixed(3)),
169
+ reason: "High-overlap active procedures differ in boundaries or context.",
170
+ });
171
+ }
172
+ }
173
+ }
174
+ return conflicts.sort((a, b) => b.score - a.score || a.aName.localeCompare(b.aName));
175
+ }
176
+
177
+ export function detectSemanticConflicts(entries: SemanticMemory[]): Array<{
178
+ aId: string;
179
+ bId: string;
180
+ aName: string;
181
+ bName: string;
182
+ reason: string;
183
+ }> {
184
+ const byId = new Map(entries.map((entry) => [entry.id, entry]));
185
+ const seen = new Set<string>();
186
+ const conflicts: Array<{
187
+ aId: string;
188
+ bId: string;
189
+ aName: string;
190
+ bName: string;
191
+ reason: string;
192
+ }> = [];
193
+ for (const entry of entries) {
194
+ for (const conflictId of entry.conflictWithIds ?? []) {
195
+ const other = byId.get(conflictId);
196
+ if (!other) continue;
197
+ const pairKey = [entry.id, other.id].sort().join("::");
198
+ if (seen.has(pairKey)) continue;
199
+ seen.add(pairKey);
200
+ conflicts.push({
201
+ aId: entry.id,
202
+ bId: other.id,
203
+ aName: entry.name,
204
+ bName: other.name,
205
+ reason: "Explicit semantic conflict link.",
206
+ });
207
+ }
208
+ }
209
+ return conflicts.sort((a, b) => a.aName.localeCompare(b.aName) || a.bName.localeCompare(b.bName));
210
+ }
211
+
212
+ // ── Alignment conflict detection ───────────────────────────
213
+
214
+ export function detectAlignmentConflicts(
215
+ entries: MemoryEntry[],
216
+ ): Array<{
217
+ aId: string;
218
+ bId: string;
219
+ reason: string;
220
+ severity: number;
221
+ recommendation: "merge" | "demote" | "forget" | "mark-situational";
222
+ rationale: string;
223
+ }> {
224
+ const candidates = entries.filter(
225
+ (entry) =>
226
+ entry.stability !== "situational" &&
227
+ (entry.retention === "core" || entry.retention === "key-event") &&
228
+ !!(entry.summary || entry.detail || entry.name),
229
+ );
230
+ const conflicts: Array<{
231
+ aId: string;
232
+ bId: string;
233
+ reason: string;
234
+ severity: number;
235
+ recommendation: "merge" | "demote" | "forget" | "mark-situational";
236
+ rationale: string;
237
+ }> = [];
238
+ const seen = new Set<string>();
239
+ const positiveMarkers = ["prefer", "always", "use", "enable", "include", "like", "keep", "should"];
240
+ const negativeMarkers = ["avoid", "never", "disable", "remove", "dislike", "hate", "stop", "do not", "don't"];
241
+ const getText = (entry: MemoryEntry) =>
242
+ `${entry.name || ""} ${entry.summary || ""} ${entry.detail || ""}`.toLowerCase();
243
+ const polarity = (text: string): "positive" | "negative" | "neutral" => {
244
+ const hasPositive = positiveMarkers.some((marker) => text.includes(marker));
245
+ const hasNegative = negativeMarkers.some((marker) => text.includes(marker));
246
+ if (hasPositive && !hasNegative) return "positive";
247
+ if (hasNegative && !hasPositive) return "negative";
248
+ return "neutral";
249
+ };
250
+
251
+ for (let i = 0; i < candidates.length; i++) {
252
+ for (let j = i + 1; j < candidates.length; j++) {
253
+ const a = candidates[i]!;
254
+ const b = candidates[j]!;
255
+ if (a.id === b.id) continue;
256
+ const overlap = tagOverlap(a.tags, b.tags);
257
+ if (overlap < 0.45) continue;
258
+ const aPolarity = polarity(getText(a));
259
+ const bPolarity = polarity(getText(b));
260
+ if (aPolarity === "neutral" || bPolarity === "neutral" || aPolarity === bPolarity) continue;
261
+ const pairKey = [a.id, b.id].sort().join(":");
262
+ if (seen.has(pairKey)) continue;
263
+ seen.add(pairKey);
264
+ const severity = Math.min(
265
+ 1,
266
+ overlap * 0.6 + ((a.salience ?? a.importance) + (b.salience ?? b.importance)) / 20 * 0.4,
267
+ );
268
+ const reason = `Potential conflict on shared context: ${aPolarity} vs ${bPolarity}`;
269
+ const recommendation = suggestConflictAction(a, b, severity);
270
+ const rationale = explainConflictAction(a, b, recommendation);
271
+ conflicts.push({ aId: a.id, bId: b.id, reason, severity, recommendation, rationale });
272
+ }
273
+ }
274
+
275
+ return conflicts.sort((a, b) => b.severity - a.severity);
276
+ }
277
+
278
+ export function suggestConflictAction(
279
+ a: MemoryEntry,
280
+ b: MemoryEntry,
281
+ severity: number,
282
+ ): "merge" | "demote" | "forget" | "mark-situational" {
283
+ const aStable = a.stability !== "situational";
284
+ const bStable = b.stability !== "situational";
285
+ const aRecent = daysSince(a.created) <= 14;
286
+ const bRecent = daysSince(b.created) <= 14;
287
+ const aLowSignal = (a.salience ?? a.importance) <= 4;
288
+ const bLowSignal = (b.salience ?? b.importance) <= 4;
289
+
290
+ if (aStable !== bStable) return "mark-situational";
291
+ if (severity >= 0.8 && (aLowSignal || bLowSignal)) return "forget";
292
+ if (severity >= 0.65 && (aRecent || bRecent)) return "demote";
293
+ return "merge";
294
+ }
295
+
296
+ export function explainConflictAction(
297
+ a: MemoryEntry,
298
+ b: MemoryEntry,
299
+ action: "merge" | "demote" | "forget" | "mark-situational",
300
+ ): string {
301
+ switch (action) {
302
+ case "mark-situational":
303
+ return "One side looks more temporary than the other, so this conflict likely comes from short-term context rather than true identity drift.";
304
+ case "forget":
305
+ return "One side has weak signal compared with the conflict risk, so forgetting the weaker memory is safer than keeping both.";
306
+ case "demote":
307
+ return "At least one side is recent enough that it may be an overfit; demoting it reduces the chance of personality drift.";
308
+ default:
309
+ return "Both memories may describe the same preference from different angles, so a merged memory is likely safer than keeping them separate.";
310
+ }
311
+ }
@@ -0,0 +1,298 @@
1
+ /**
2
+ * [WHO]: Provides selectRecallEntries — budget/tier/pick logic for progressive recall
3
+ * [FROM]: Depends on ./scoring.js for pickTop, scoreEntry, scoreEpisode, scoreWorkEntry, tierEntries; ./linking.js for getGraphNeighborhoodBySeeds; ./engine-scoring-v2.js for V2 scoring; ./engine-injection-text.js for conversation prefs
4
+ * [TO]: Consumed by engine.ts (getMemoryInjection)
5
+ * [HERE]: packages/mem-core/src/engine-recall-select.ts - recall entry selection and budget allocation
6
+ */
7
+
8
+ import { selectConversationPreferences, mergeUniqueEntries } from "./engine-injection-text.js";
9
+ import {
10
+ scoreEpisodeMemory,
11
+ scoreEpisodeFacet,
12
+ scoreV2SemanticMemory,
13
+ scoreProceduralMemory,
14
+ } from "./engine-scoring-v2.js";
15
+ import { getGraphNeighborhoodBySeeds, type GraphNeighbor } from "./linking.js";
16
+ import {
17
+ pickTop,
18
+ scoreEntry,
19
+ scoreEpisode,
20
+ scoreWorkEntry,
21
+ tierEntries,
22
+ } from "./scoring.js";
23
+ import type { ProgressiveRecallConfig } from "./config.js";
24
+ import type { MemoryEntry, MemoryScope, WorkEntry, Episode } from "./types.js";
25
+ import type { EpisodeFacet, EpisodeMemory, ProceduralMemory, SemanticMemory } from "./types-v2.js";
26
+
27
+ export interface RecallSelectionResult {
28
+ active: {
29
+ knowledge: MemoryEntry[];
30
+ lessons: MemoryEntry[];
31
+ events: MemoryEntry[];
32
+ preferences: MemoryEntry[];
33
+ facets: MemoryEntry[];
34
+ episodeMemories: EpisodeMemory[];
35
+ episodeFacets: EpisodeFacet[];
36
+ semanticMemories: SemanticMemory[];
37
+ procedural: ProceduralMemory[];
38
+ };
39
+ cue: {
40
+ knowledge: MemoryEntry[];
41
+ lessons: MemoryEntry[];
42
+ events: MemoryEntry[];
43
+ preferences: MemoryEntry[];
44
+ facets: MemoryEntry[];
45
+ episodes: Episode[];
46
+ work: WorkEntry[];
47
+ episodeMemories: EpisodeMemory[];
48
+ episodeFacets: EpisodeFacet[];
49
+ semanticMemories: SemanticMemory[];
50
+ procedural: ProceduralMemory[];
51
+ };
52
+ allEntries: MemoryEntry[];
53
+ graphContext: GraphNeighbor[];
54
+ // For reinforcement
55
+ injectedActiveKnowledge: MemoryEntry[];
56
+ injectedActiveLessons: MemoryEntry[];
57
+ injectedActiveEvents: MemoryEntry[];
58
+ injectedActivePrefs: MemoryEntry[];
59
+ injectedCueKnowledge: MemoryEntry[];
60
+ injectedCueLessons: MemoryEntry[];
61
+ injectedCueEvents: MemoryEntry[];
62
+ injectedCuePrefs: MemoryEntry[];
63
+ injectedCueEpisodes: Episode[];
64
+ legacyFacetBridgeActive: MemoryEntry[];
65
+ legacyFacetBridgeCue: MemoryEntry[];
66
+ }
67
+
68
+ export function selectRecallEntries(params: {
69
+ // Legacy entries (filtered)
70
+ knowledge: MemoryEntry[]; lessons: MemoryEntry[]; events: MemoryEntry[];
71
+ prefs: MemoryEntry[]; facets: MemoryEntry[]; work: WorkEntry[]; episodes: Episode[];
72
+ // V2 entries (filtered)
73
+ v2Episodes: EpisodeMemory[]; v2EpisodeFacets: EpisodeFacet[];
74
+ v2Semantic: SemanticMemory[]; procedural: ProceduralMemory[];
75
+ // Raw entries for graph context
76
+ allKnowledge: MemoryEntry[]; allLessons: MemoryEntry[]; allEvents: MemoryEntry[];
77
+ allPrefs: MemoryEntry[]; allFacets: MemoryEntry[];
78
+ // Semantic candidates from embedding search
79
+ semanticCandidates: Array<{ memoryId: string; memoryKind: string; score: number }>;
80
+ // Config
81
+ halfLife: Record<string, number>;
82
+ scoreWeights: { recency: number; importance: number; relevance: number };
83
+ progressiveRecall: ProgressiveRecallConfig;
84
+ tokenBudget: number;
85
+ structuralWeight: number;
86
+ project: string;
87
+ contextTags: string[];
88
+ }): RecallSelectionResult {
89
+ const {
90
+ knowledge, lessons, events, prefs, facets, work, episodes,
91
+ v2Episodes, v2EpisodeFacets, v2Semantic, procedural,
92
+ allKnowledge, allLessons, allEvents, allPrefs, allFacets,
93
+ semanticCandidates, halfLife: hl, scoreWeights: sw, progressiveRecall: pr,
94
+ tokenBudget, structuralWeight, project, contextTags,
95
+ } = params;
96
+
97
+ const semanticEpisodeIds = new Set(
98
+ semanticCandidates.filter((item) => item.memoryKind === "episode").map((item) => item.memoryId),
99
+ );
100
+ const semanticFacetIds = new Set(
101
+ semanticCandidates.filter((item) => item.memoryKind === "facet").map((item) => item.memoryId),
102
+ );
103
+ const semanticProcedureIds = new Set(
104
+ semanticCandidates.filter((item) => item.memoryKind === "procedural").map((item) => item.memoryId),
105
+ );
106
+ const semanticV2SemanticIds = new Set(
107
+ semanticCandidates.filter((item) => item.memoryKind === "semantic").map((item) => item.memoryId),
108
+ );
109
+
110
+ // Tier all MemoryEntry categories
111
+ const tieredKnowledge = tierEntries(knowledge, project, contextTags, hl, sw, pr);
112
+ const tieredLessons = tierEntries(lessons, project, contextTags, hl, sw, pr);
113
+ const tieredEvents = tierEntries(events, project, contextTags, hl, sw, pr);
114
+ const tieredPrefs = tierEntries(prefs, project, contextTags, hl, sw, pr);
115
+ const tieredFacets = tierEntries(facets, project, contextTags, hl, sw, pr);
116
+ const forcedConversationPrefs = selectConversationPreferences(prefs);
117
+
118
+ // Budget calculation
119
+ const totalChars = tokenBudget * 4;
120
+ const activeChars = Math.floor(totalChars * pr.budgetActive);
121
+ const cueChars = Math.floor(totalChars * pr.budgetCue);
122
+ const proceduralChars = Math.floor(totalChars * 0.18);
123
+
124
+ // Entry length helpers
125
+ const activeLen = (e: MemoryEntry) =>
126
+ (e.name?.length || 0) + (e.summary?.length || 0) + (e.detail?.length || 0) + 30;
127
+ const cueLen = (e: MemoryEntry) => (e.name?.length || 0) + (e.summary?.length || 0) + 30;
128
+ const scoreFn = (e: MemoryEntry) => scoreEntry(e, project, contextTags, hl, sw);
129
+ const proceduralLen = (pItem: ProceduralMemory) =>
130
+ (pItem.name?.length || 0) + (pItem.summary?.length || 0) + pItem.steps.reduce((sum, step) => sum + step.text.length, 0) + 60;
131
+ const proceduralCueLen = (pItem: ProceduralMemory) =>
132
+ (pItem.name?.length || 0) + (pItem.summary?.length || 0) + 40;
133
+ const proceduralScoreFn = (pItem: ProceduralMemory) => scoreProceduralMemory(pItem, project, contextTags, structuralWeight);
134
+ const episodeMemoryLen = (item: EpisodeMemory) => (item.title?.length || 0) + item.summary.length + 40;
135
+ const episodeFacetLen = (item: EpisodeFacet) =>
136
+ item.searchText.length + (item.anchorText?.length || 0) + (item.summary?.length || 0) + 40;
137
+ const episodeCueLen = (item: EpisodeMemory) => (item.title?.length || 0) + item.summary.length + 36;
138
+ const episodeFacetCueLen = (item: EpisodeFacet) =>
139
+ item.searchText.length + (item.summary?.length || 0) + (item.anchorText?.length || 0) + 36;
140
+ const semanticMemoryLen = (item: SemanticMemory) =>
141
+ (item.name?.length || 0) + item.summary.length + (item.detail?.length || 0) + 40;
142
+ const semanticMemoryCueLen = (item: SemanticMemory) => (item.name?.length || 0) + item.summary.length + 36;
143
+ const episodeScoreFn = (item: EpisodeMemory) => scoreEpisodeMemory(item, project, contextTags, structuralWeight);
144
+ const episodeFacetScoreFn = (item: EpisodeFacet) => scoreEpisodeFacet(item, project, contextTags, structuralWeight);
145
+ const semanticMemoryScoreFn = (item: SemanticMemory) => scoreV2SemanticMemory(item, project, contextTags, structuralWeight);
146
+
147
+ // Active tier
148
+ const activeBudgetPer = Math.floor(activeChars / 5);
149
+ const activeKnowledge = pickTop(tieredKnowledge.active, scoreFn, activeLen, activeBudgetPer);
150
+ const activeLessons = pickTop(tieredLessons.active, scoreFn, activeLen, activeBudgetPer);
151
+ const activeEvents = pickTop(tieredEvents.active, scoreFn, activeLen, activeBudgetPer);
152
+ const activePrefs = mergeUniqueEntries(
153
+ forcedConversationPrefs,
154
+ pickTop(tieredPrefs.active, scoreFn, activeLen, activeBudgetPer),
155
+ );
156
+ const activeFacets = pickTop(tieredFacets.active, scoreFn, activeLen, activeBudgetPer);
157
+ const activeProcedural = pickTop(
158
+ procedural.filter((item) => semanticProcedureIds.has(item.id) || proceduralScoreFn(item) >= 0.45),
159
+ proceduralScoreFn,
160
+ proceduralLen,
161
+ Math.max(400, Math.floor(proceduralChars * 0.55)),
162
+ );
163
+ const activeEpisodeMemories = pickTop(
164
+ v2Episodes.filter((item) => semanticEpisodeIds.has(item.id) || episodeScoreFn(item) >= 0.45),
165
+ episodeScoreFn,
166
+ episodeMemoryLen,
167
+ Math.max(320, Math.floor(activeChars * 0.18)),
168
+ );
169
+ const activeEpisodeFacets = pickTop(
170
+ v2EpisodeFacets.filter((item) => semanticFacetIds.has(item.id) || episodeFacetScoreFn(item) >= 0.42),
171
+ episodeFacetScoreFn,
172
+ episodeFacetLen,
173
+ Math.max(320, Math.floor(activeChars * 0.18)),
174
+ );
175
+ const activeSemanticMemories = pickTop(
176
+ v2Semantic.filter((item) => semanticV2SemanticIds.has(item.id) || semanticMemoryScoreFn(item) >= 0.42),
177
+ semanticMemoryScoreFn,
178
+ semanticMemoryLen,
179
+ Math.max(280, Math.floor(activeChars * 0.16)),
180
+ );
181
+
182
+ const activeSeeds = [...activeKnowledge, ...activeLessons, ...activeEvents, ...activePrefs, ...activeFacets];
183
+ const allEntries = [...allKnowledge, ...allLessons, ...allEvents, ...allPrefs, ...allFacets];
184
+ const activeIds = new Set(activeSeeds.map((entry) => entry.id));
185
+ const graphContext = getGraphNeighborhoodBySeeds(activeSeeds, allEntries, 8).filter(
186
+ (neighbor) => !activeIds.has(neighbor.entry.id),
187
+ );
188
+
189
+ // Cue tier
190
+ const cueBudgetPer = Math.floor(cueChars / 7);
191
+ const cueKnowledge = pickTop(tieredKnowledge.cue, scoreFn, cueLen, cueBudgetPer);
192
+ const cueLessons = pickTop(tieredLessons.cue, scoreFn, cueLen, cueBudgetPer);
193
+ const cueEvents = pickTop(tieredEvents.cue, scoreFn, cueLen, cueBudgetPer);
194
+ const forcedConversationPrefIds = new Set(forcedConversationPrefs.map((entry) => entry.id));
195
+ const cuePrefs = pickTop(
196
+ tieredPrefs.cue.filter((entry) => !forcedConversationPrefIds.has(entry.id)),
197
+ scoreFn,
198
+ cueLen,
199
+ cueBudgetPer,
200
+ );
201
+ const cueFacets = pickTop(tieredFacets.cue, scoreFn, cueLen, cueBudgetPer);
202
+ const graphContextIds = new Set(graphContext.map((neighbor) => neighbor.entry.id));
203
+ const dedupeCue = (entries: MemoryEntry[]) => entries.filter((entry) => !graphContextIds.has(entry.id));
204
+ const dedupedCueKnowledge = dedupeCue(cueKnowledge);
205
+ const dedupedCueLessons = dedupeCue(cueLessons);
206
+ const dedupedCueEvents = dedupeCue(cueEvents);
207
+ const dedupedCuePrefs = dedupeCue(cuePrefs);
208
+ const dedupedCueFacets = dedupeCue(cueFacets);
209
+
210
+ const topEpisodes = pickTop(
211
+ episodes,
212
+ (ep) => scoreEpisode(ep, project, contextTags, hl, sw),
213
+ (ep) => ep.summary.length + 30,
214
+ cueBudgetPer,
215
+ );
216
+ const topWork = pickTop(
217
+ work,
218
+ (w) => scoreWorkEntry(w, project, contextTags, hl, sw),
219
+ (w) => w.goal.length + w.summary.length + 30,
220
+ cueBudgetPer,
221
+ );
222
+ const cueProcedural = pickTop(
223
+ procedural.filter((item) => !activeProcedural.some((activeItem) => activeItem.id === item.id)),
224
+ proceduralScoreFn,
225
+ proceduralCueLen,
226
+ Math.max(280, Math.floor(proceduralChars * 0.45)),
227
+ );
228
+ const cueEpisodeMemories = pickTop(
229
+ v2Episodes.filter((item) => !activeEpisodeMemories.some((activeItem) => activeItem.id === item.id)),
230
+ episodeScoreFn,
231
+ episodeCueLen,
232
+ Math.max(220, Math.floor(cueChars * 0.12)),
233
+ );
234
+ const cueEpisodeFacets = pickTop(
235
+ v2EpisodeFacets.filter((item) => !activeEpisodeFacets.some((activeItem) => activeItem.id === item.id)),
236
+ episodeFacetScoreFn,
237
+ episodeFacetCueLen,
238
+ Math.max(220, Math.floor(cueChars * 0.12)),
239
+ );
240
+ const cueSemanticMemories = pickTop(
241
+ v2Semantic.filter((item) => !activeSemanticMemories.some((activeItem) => activeItem.id === item.id)),
242
+ semanticMemoryScoreFn,
243
+ semanticMemoryCueLen,
244
+ Math.max(220, Math.floor(cueChars * 0.12)),
245
+ );
246
+
247
+ const legacyFacetBridgeActive = activeFacets.filter(
248
+ (entry) => entry.facetData?.kind === "pattern" || entry.facetData?.kind === "struggle",
249
+ );
250
+ const legacyFacetBridgeCue = dedupedCueFacets.filter(
251
+ (entry) => entry.facetData?.kind === "pattern" || entry.facetData?.kind === "struggle",
252
+ );
253
+ const v2SignalCount =
254
+ activeEpisodeMemories.length + activeEpisodeFacets.length + activeSemanticMemories.length + activeProcedural.length +
255
+ cueEpisodeMemories.length + cueEpisodeFacets.length + cueSemanticMemories.length + cueProcedural.length;
256
+ const useLegacyFallback = v2SignalCount === 0;
257
+ const injectedActiveKnowledge = useLegacyFallback ? activeKnowledge : [];
258
+ const injectedActiveLessons = useLegacyFallback ? activeLessons : [];
259
+ const injectedActiveEvents = useLegacyFallback ? activeEvents : [];
260
+ const injectedActivePrefs = useLegacyFallback ? activePrefs : [];
261
+ const injectedCueKnowledge = useLegacyFallback ? dedupedCueKnowledge : [];
262
+ const injectedCueLessons = useLegacyFallback ? dedupedCueLessons : [];
263
+ const injectedCueEvents = useLegacyFallback ? dedupedCueEvents : [];
264
+ const injectedCuePrefs = useLegacyFallback ? dedupedCuePrefs : [];
265
+ const injectedCueEpisodes = useLegacyFallback ? topEpisodes : [];
266
+
267
+ return {
268
+ active: {
269
+ knowledge: injectedActiveKnowledge,
270
+ lessons: injectedActiveLessons,
271
+ events: injectedActiveEvents,
272
+ preferences: injectedActivePrefs,
273
+ facets: legacyFacetBridgeActive,
274
+ episodeMemories: activeEpisodeMemories,
275
+ episodeFacets: activeEpisodeFacets,
276
+ semanticMemories: activeSemanticMemories,
277
+ procedural: activeProcedural,
278
+ },
279
+ cue: {
280
+ knowledge: injectedCueKnowledge,
281
+ lessons: injectedCueLessons,
282
+ events: injectedCueEvents,
283
+ preferences: injectedCuePrefs,
284
+ facets: legacyFacetBridgeCue,
285
+ episodes: injectedCueEpisodes,
286
+ work: topWork,
287
+ episodeMemories: cueEpisodeMemories,
288
+ episodeFacets: cueEpisodeFacets,
289
+ semanticMemories: cueSemanticMemories,
290
+ procedural: cueProcedural,
291
+ },
292
+ allEntries,
293
+ graphContext,
294
+ injectedActiveKnowledge, injectedActiveLessons, injectedActiveEvents, injectedActivePrefs,
295
+ injectedCueKnowledge, injectedCueLessons, injectedCueEvents, injectedCuePrefs, injectedCueEpisodes,
296
+ legacyFacetBridgeActive, legacyFacetBridgeCue,
297
+ };
298
+ }