memories-lite 0.10.1 → 0.99.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -16,10 +16,8 @@ import {
16
16
  HistoryManagerFactory,
17
17
  } from "../utils/factory";
18
18
  import {
19
- FactRetrievalSchema_extended,
20
- getFactRetrievalMessages,
21
- getUpdateMemoryMessages,
22
- MemoryUpdateSchema,
19
+ DiscussionSynthesisSchema,
20
+ getDiscussionSynthesisMessages,
23
21
  removeCodeBlocks,
24
22
  } from "../prompts";
25
23
  import { DummyHistoryManager } from "../storage/DummyHistoryManager";
@@ -152,180 +150,98 @@ export class MemoriesLite {
152
150
  }
153
151
 
154
152
 
153
+ /**
154
+ * Capture une discussion et génère une synthèse (title + summary)
155
+ * Utilise getDiscussionSynthesisMessages pour produire la synthèse
156
+ */
155
157
  private async addToVectorStore(
156
158
  messages: Message[],
157
159
  metadata: Record<string, any>,
158
160
  userId: string,
159
161
  filters: SearchFilters,
160
- customFacts?: string,
162
+ capturePrompt?: string,
161
163
  ): Promise<MemoryItem[]> {
162
164
 
163
165
  const $t = this.$t;
164
166
  const vectorStore = await this.getVectorStore(userId);
165
- const parsedMessages = messages.filter((m) => typeof m.content === 'string' && m.role=='user').map((m) => `${m.role=='user' ? '**USER**: ' : '**ASSISTANT**: '}${$t(m.content as string)}\n`).join("\n");
166
-
167
- // Disinterest handling is delegated to the LLM via prompt guidelines
167
+
168
+ //
169
+ // Formater les messages pour la synthèse
170
+ const parsedMessages = messages
171
+ .filter((m) => typeof m.content === 'string')
172
+ .map((m) => `**${m.role.toUpperCase()}**: ${$t(m.content as string)}`)
173
+ .join("\n\n");
168
174
 
169
- const [systemPrompt, userPrompt] = getFactRetrievalMessages(parsedMessages, customFacts||this.customPrompt);
175
+ //
176
+ // Générer la synthèse via LLM
177
+ const [systemPrompt, userPrompt] = getDiscussionSynthesisMessages(
178
+ parsedMessages,
179
+ capturePrompt || this.customPrompt
180
+ );
170
181
 
171
182
  const response = await this.llm.generateResponse(
172
183
  [
173
184
  { role: "system", content: systemPrompt },
174
185
  { role: "user", content: userPrompt },
175
186
  ],
176
- {...zodResponseFormat(FactRetrievalSchema_extended,"FactRetrieval")},[],false
187
+ { ...zodResponseFormat(DiscussionSynthesisSchema, "DiscussionSynthesis") },
188
+ [],
189
+ false
177
190
  );
178
- const parsedResponse = (response:any) => {
191
+
192
+ //
193
+ // Parser la réponse
194
+ const parsedResponse = (res: any) => {
179
195
  try {
180
- // structured output
181
- if(typeof response === 'object') {
182
- return response;
196
+ if (typeof res === 'object') {
197
+ return res;
183
198
  }
184
- const cleanResponse = removeCodeBlocks(response as string);
199
+ const cleanResponse = removeCodeBlocks(res as string);
185
200
  return JSON.parse(cleanResponse);
186
201
  } catch (e) {
187
- console.error(
188
- "Failed to parse facts from LLM response:",
189
- response,
190
- e,
191
- response
192
- );
193
- return [];
202
+ console.error("Failed to parse synthesis from LLM response:", res, e);
203
+ return { title: "Sans titre", summary: "" };
194
204
  }
195
- }
196
- //
197
- // can use native structured output
198
- // Drop factual facts at capture level (do not store factual memories)
199
- // FIXME Drop factual should be done at prompt level
200
- const facts = parsedResponse(response).facts?.filter((f:any) => !f.existing )||[];
205
+ };
201
206
 
202
- // console.log("-- DBG extract:", userPrompt);
203
- // console.log("-- DBG facts:", facts);
207
+ const { title, summary } = parsedResponse(response);
204
208
 
205
- // Get embeddings for new facts
206
- const newMessageEmbeddings: Record<string, number[]> = {};
207
- const retrievedOldMemory: Array<{ id: string; text: string; type: string }> = [];
209
+ if (!summary) {
210
+ console.warn("-- ⚠️ Empty summary from LLM, skipping memory creation");
211
+ return [];
212
+ }
208
213
 
209
214
  //
210
- // add the userId to the filters
211
- filters.userId = userId;
212
- // Create embeddings and search for similar memories
213
- for (const elem of facts) {
214
- const fact = elem.fact;
215
- const embedding = await this.embedder.embed(fact);
216
- newMessageEmbeddings[fact] = embedding;
217
-
218
- const existingMemories = await vectorStore.search(
219
- embedding,
220
- 5,
221
- filters,
222
- );
223
- for (const mem of existingMemories) {
224
- retrievedOldMemory.push({ id: mem.id, text: mem.payload.data,type: mem.payload.type });
225
- }
226
- }
215
+ // Créer l'embedding sur le summary (pour recherche sémantique)
216
+ const embedding = await this.embedder.embed(summary);
227
217
 
228
- // console.log("-- DBG old memories:", retrievedOldMemory);
229
- // Remove duplicates from old memories
230
- const uniqueOldMemories = retrievedOldMemory.filter(
231
- (mem, index) =>
232
- retrievedOldMemory.findIndex((m) => m.id === mem.id) === index,
233
- );
218
+ //
219
+ // Préparer les métadonnées
220
+ const memoryType: MemoryType = metadata.type || 'discussion';
221
+ const memoryMetadata = {
222
+ ...metadata,
223
+ title,
224
+ type: memoryType,
225
+ userId,
226
+ };
234
227
 
235
- // Create UUID mapping for handling UUID hallucinations
236
- const tempUuidMapping: Record<string, string> = {};
237
- uniqueOldMemories.forEach((item, idx) => {
238
- tempUuidMapping[String(idx)] = item.id;
239
- uniqueOldMemories[idx].id = String(idx);
240
- });
241
-
242
- // Get memory update decisions
243
- const lastUserMessage = [...messages].reverse().find(m => m.role === 'user');
244
- const userInstruction = typeof lastUserMessage?.content === 'string' ? lastUserMessage?.content as string : '';
245
- const updatePrompt = getUpdateMemoryMessages(uniqueOldMemories, facts, 'French', userInstruction);
246
-
247
- // console.log("-- DBG updatePrompt:", updatePrompt);
248
- const updateResponse = await this.llm.generateResponse(
249
- [{ role: "user", content: updatePrompt }],
250
- {...zodResponseFormat(MemoryUpdateSchema,"Memory")},[],false,
228
+ //
229
+ // Stocker la mémoire
230
+ const memoryId = await this.createMemory(
231
+ summary,
232
+ { [summary]: embedding },
233
+ memoryMetadata,
234
+ userId,
251
235
  );
252
- // console.log("-- DBG merge:", updatePrompt);
253
236
 
254
- const memoryActions: any[] = parsedResponse(updateResponse).memory || [];
237
+ console.log(`-- 🧠 Memory created: "${title}" (${memoryType})`);
255
238
 
256
- // Process memory actions
257
- const results: MemoryItem[] = [];
258
- for (const action of memoryActions) {
259
- // Ignore any factual memory actions (ADD/UPDATE/DELETE) → void
260
- if(action.type === 'factual') {
261
- continue;
262
- }
263
- if(action.reason === "undefined") {
264
- console.log(`-- ⛔ LLM Error: ${action.event}, ${action.type}, "${action.text}"`);
265
- continue;
266
- }
267
- console.log(`-- DBG memory "${userId}": ${action.event}, ${action.type}, "${action.text}", why: "${action.reason}"`);
268
- try {
269
- switch (action.event) {
270
- case "ADD": {
271
- if(!action.type) {
272
- // log error
273
- console.error("Type is mandatory to manage memories:", action);
274
- continue;
275
- }
276
- metadata.type = action.type;
277
- const memoryId = await this.createMemory(
278
- action.text,
279
- newMessageEmbeddings,
280
- metadata,
281
- userId,
282
- );
283
- results.push({
284
- id: memoryId,
285
- memory: action.text,
286
- type: action.type,
287
- metadata: { event: action.event },
288
- });
289
- break;
290
- }
291
- case "UPDATE": {
292
- const realMemoryId = tempUuidMapping[action.id];
293
- const type = metadata.type = uniqueOldMemories[action.id].type || action.type;
294
- await this.updateMemory(
295
- realMemoryId,
296
- action.text,
297
- newMessageEmbeddings,
298
- metadata,
299
- userId,
300
- );
301
- results.push({
302
- id: realMemoryId,
303
- memory: action.text,
304
- type,
305
- metadata: {
306
- event: action.event,
307
- previousMemory: action.old_memory,
308
- },
309
- });
310
- break;
311
- }
312
- case "DELETE": {
313
- const realMemoryId = tempUuidMapping[action.id];
314
- await this.deleteMemory(realMemoryId, userId);
315
- results.push({
316
- id: realMemoryId,
317
- memory: action.text,
318
- type: action.type,
319
- metadata: { event: action.event },
320
- });
321
- break;
322
- }
323
- }
324
- } catch (error) {
325
- console.error(`Error processing memory action: ${error}`);
326
- }
327
- }
328
- return results;
239
+ return [{
240
+ id: memoryId,
241
+ memory: summary,
242
+ type: memoryType,
243
+ metadata: { title, event: "ADD" },
244
+ }];
329
245
  }
330
246
 
331
247
  static fromConfig(configDict: Record<string, any>): MemoriesLite {
@@ -342,24 +258,24 @@ export class MemoriesLite {
342
258
  return LiteVectorStore.from(userId, this.vectorStoreConfig);
343
259
  }
344
260
 
261
+ /**
262
+ * Capture une discussion et génère une mémoire (title + summary)
263
+ *
264
+ * @param messages - Messages de la discussion ou texte brut
265
+ * @param userId - ID utilisateur
266
+ * @param config - Options incluant capturePrompt pour personnaliser la synthèse
267
+ */
345
268
  async capture(
346
269
  messages: string | Message[],
347
270
  userId: string,
348
271
  config: AddMemoryOptions,
349
272
  ): Promise<SearchResult> {
350
- // await this._captureEvent("add", {
351
- // message_count: Array.isArray(messages) ? messages.length : 1,
352
- // has_metadata: !!config.metadata,
353
- // has_filters: !!config.filters,
354
- // infer: config.infer,
355
- // });
356
273
  const {
357
274
  agentId,
358
275
  runId,
359
276
  metadata = {},
360
277
  filters = {},
361
- infer = true,
362
- customFacts
278
+ capturePrompt
363
279
  } = config;
364
280
 
365
281
  if (agentId) filters.agentId = metadata.agentId = agentId;
@@ -377,16 +293,18 @@ export class MemoriesLite {
377
293
 
378
294
  const final_parsedMessages = await parse_vision_messages(parsedMessages);
379
295
 
380
- // Add to vector store
296
+ //
297
+ // Générer synthèse et stocker
381
298
  const vectorStoreResult = await this.addToVectorStore(
382
299
  final_parsedMessages,
383
300
  metadata,
384
301
  userId,
385
302
  filters,
386
- customFacts,
303
+ capturePrompt,
387
304
  );
388
305
 
389
- // Add to graph store if available
306
+ //
307
+ // Graph store (si configuré)
390
308
  let graphResult;
391
309
  if (this.graphMemory) {
392
310
  try {
@@ -10,8 +10,7 @@ export interface Entity {
10
10
  export interface AddMemoryOptions extends Entity {
11
11
  metadata?: Record<string, any>;
12
12
  filters?: SearchFilters;
13
- customFacts?: string;
14
- infer?: boolean;
13
+ capturePrompt?: string;
15
14
  }
16
15
 
17
16
  export interface SearchMemoryOptions extends Entity {
@@ -1,36 +1,83 @@
1
1
  import { z } from "zod";
2
2
  import { MemoryItem } from "../types";
3
3
 
4
- // Define Zod schema for fact retrieval output
4
+ // ══════════════════════════════════════════════════════════════════════════════
5
+ // DISCUSSION SYNTHESIS - Nouveau système de mémoire
6
+ // ══════════════════════════════════════════════════════════════════════════════
7
+
8
+ /**
9
+ * Schema pour la synthèse de discussion
10
+ * Produit un titre court et une synthèse opérationnelle
11
+ */
12
+ export const DiscussionSynthesisSchema = z.object({
13
+ title: z.string().describe("Titre court et descriptif (6-10 mots)"),
14
+ summary: z.string().describe("Synthèse opérationnelle des points clés (50-100 mots)")
15
+ });
16
+
17
+ /**
18
+ * Prompt par défaut pour la synthèse de discussion
19
+ * Peut être remplacé via capturePrompt dans AddMemoryOptions
20
+ */
21
+ export const DEFAULT_DISCUSSION_PROMPT = `Tu es un expert en synthèse opérationnelle.
22
+
23
+ À partir de cette discussion, génère :
24
+ 1. TITRE: Un titre court et descriptif (6-10 mots) qui capture l'essence de la demande
25
+ 2. SUMMARY: Les points clés du chemin de résolution (50-100 mots)
26
+
27
+ Cette synthèse servira à retrouver et réappliquer ce pattern de résolution similaire.
28
+ Utilise la même langue que la discussion.
29
+
30
+ Discussion à synthétiser:
31
+ `;
32
+
33
+ /**
34
+ * Génère les messages pour la synthèse de discussion
35
+ * @param discussion - Contenu de la discussion formatée
36
+ * @param capturePrompt - Prompt custom optionnel (remplace DEFAULT_DISCUSSION_PROMPT)
37
+ * @returns [systemPrompt, userPrompt]
38
+ */
39
+ export function getDiscussionSynthesisMessages(
40
+ discussion: string,
41
+ capturePrompt?: string
42
+ ): [string, string] {
43
+ const systemPrompt = capturePrompt || DEFAULT_DISCUSSION_PROMPT;
44
+ return [systemPrompt, discussion];
45
+ }
46
+
47
+ // ══════════════════════════════════════════════════════════════════════════════
48
+ // @deprecated - Ancien système de capture (todo, factual)
49
+ // Ces exports sont conservés pour compatibilité mais ne doivent plus être utilisés
50
+ // ══════════════════════════════════════════════════════════════════════════════
51
+
52
+ /**
53
+ * @deprecated Use DiscussionSynthesisSchema instead
54
+ */
5
55
  export const FactRetrievalSchema_simple = z.object({
6
56
  facts: z
7
57
  .array(z.string())
8
58
  .describe("An array of distinct facts extracted from the conversation."),
9
59
  });
10
60
 
11
-
12
- //1. **Factual memory** stable facts & preferences about the user
13
- //2. **Todo memory** explicit user tasks to remember
14
- //3. **Assistant preference memory** – how the user wants the AI to behave
15
- //
61
+ /**
62
+ * @deprecated Use DiscussionSynthesisSchema instead
63
+ * Types todo et factual supprimés - seul assistant_preference reste pour compatibilité
64
+ */
16
65
  export const FactRetrievalSchema_extended = z.object({
17
66
  facts: z
18
67
  .array(
19
68
  z.object({
20
69
  fact: z.string().describe("The fact extracted from the conversation."),
21
70
  existing: z.boolean().describe("Whether the fact is already present"),
22
- type: z.enum(["assistant_preference","factual","todo"])
23
- .describe(`The type of the fact.
24
- Use 'assistant_preference' for Assistant behavior preferences (style/language/constraints/commands).
25
- Use 'factual' for stable user facts (identity, preferences, beliefs, work context).
26
- Use 'todo' ONLY if the user explicitly asks to save/keep as a todo (e.g., « garde/enregistre en todo », « ajoute un todo »). Do not infer todos.
27
- `),
71
+ type: z.enum(["assistant_preference"])
72
+ .describe(`The type of the fact. Only 'assistant_preference' is supported.`),
28
73
  })
29
74
  )
30
75
  });
31
76
 
32
77
 
33
- // Define Zod schema for memory update output
78
+ /**
79
+ * @deprecated Memory updates are disabled - use capture() for new memories
80
+ */
34
81
  export const MemoryUpdateSchema = z.object({
35
82
  memory: z
36
83
  .array(
@@ -54,21 +101,14 @@ export const MemoryUpdateSchema = z.object({
54
101
  "The reason why you selected this event.",
55
102
  ),
56
103
  type: z
57
- .enum(["factual", "todo", "assistant_preference"])
58
- .describe("Type of the memory. Use 'assistant_preference' for Assistant behavior preferences, 'factual' for user facts, 'todo' for explicit tasks."),
104
+ .enum(["assistant_preference"])
105
+ .describe("Type of the memory. Only 'assistant_preference' is supported."),
59
106
  }),
60
107
  )
61
108
  .describe(
62
109
  "An array representing the state of memory items after processing new facts.",
63
110
  ),
64
111
  });
65
- /**
66
- * Practical Application:
67
- *
68
- * If the task is "factual" (e.g., "Where do I live?", "What's my job?") → retrieve factual memory.
69
- * If the task is about assistant behavior (e.g., "How should I respond?") → retrieve assistant_preference memory.
70
- * If the task is a user task/reminder (e.g., "Add a reminder to call the bank tomorrow") → retrieve todo memory.
71
- */
72
112
  export const MEMORY_STRING_SYSTEM = `# DIRECTIVES FOR MEMORIES
73
113
  - Information stored in memory is always enclosed within the <memories> tag.
74
114
  - Prioritize the latest user message over memories (the user's current question is authoritative).
@@ -80,10 +120,10 @@ export const MEMORY_STRING_SYSTEM = `# DIRECTIVES FOR MEMORIES
80
120
 
81
121
  export const MEMORY_STRING_PREFIX = "Use these contextual memories to guide your response. Prioritize the user's question. Ignore irrelevant memories."
82
122
 
83
-
84
- // Deprecated: getFactRetrievalMessages_O removed in favor of getFactRetrievalMessages
85
-
86
-
123
+ /**
124
+ * @deprecated Use getDiscussionSynthesisMessages instead
125
+ * Cette fonction est conservée pour compatibilité avec l'ancien système
126
+ */
87
127
  export function getFactRetrievalMessages(
88
128
  parsedMessages: string,
89
129
  customRules: string = "",
@@ -132,6 +172,9 @@ Remember the following:
132
172
  return [systemPrompt, userPrompt];
133
173
  }
134
174
 
175
+ /**
176
+ * @deprecated Memory updates are disabled by config
177
+ */
135
178
  export function getUpdateMemoryMessages(
136
179
  retrievedOldMemory: Array<{ id: string; text: string }>,
137
180
  newRetrievedFacts: any[],
@@ -72,9 +72,8 @@ export interface MemoryTypeConfig {
72
72
  }
73
73
 
74
74
  export interface MemoryScoringConfig {
75
- todo: MemoryTypeConfig;
76
- factual: MemoryTypeConfig;
77
75
  assistant_preference: MemoryTypeConfig;
76
+ discussion: MemoryTypeConfig;
78
77
  default: MemoryTypeConfig; // Fallback if type is missing or unknown
79
78
  }
80
79
 
@@ -101,7 +100,7 @@ export interface MemoryConfig {
101
100
  enableGraph?: boolean;
102
101
  }
103
102
 
104
- export type MemoryType = 'todo' | 'factual' | 'assistant_preference';
103
+ export type MemoryType = 'assistant_preference' | 'discussion';
105
104
 
106
105
  export interface MemoryItem {
107
106
  id: string;
@@ -162,19 +161,13 @@ export const MemoryConfigSchema = z.object({
162
161
  dimension: z.number().optional(),
163
162
  client: z.any().optional(),
164
163
  scoring: z.object({
165
- todo: z.object({
166
- alpha: z.number(),
167
- beta: z.number(),
168
- gamma: z.number(),
169
- halfLifeDays: z.number(),
170
- }),
171
- factual: z.object({
164
+ assistant_preference: z.object({
172
165
  alpha: z.number(),
173
166
  beta: z.number(),
174
167
  gamma: z.number(),
175
168
  halfLifeDays: z.number(),
176
169
  }),
177
- assistant_preference: z.object({
170
+ discussion: z.object({
178
171
  alpha: z.number(),
179
172
  beta: z.number(),
180
173
  gamma: z.number(),
@@ -25,6 +25,10 @@ jest.mock('sqlite3', () => {
25
25
  });
26
26
 
27
27
 
28
+ /**
29
+ * Tests for LiteVectorStore private methods
30
+ * Updated to use new memory types: assistant_preference and discussion
31
+ */
28
32
  describe('LiteVectorStore Private Methods', () => {
29
33
  let store: LiteVectorStore;
30
34
  const userId = 'test-user';
@@ -98,59 +102,65 @@ describe('LiteVectorStore Private Methods', () => {
98
102
  const veryOldDate = new Date(now.getTime() - 400 * 24 * 60 * 60 * 1000).toISOString();
99
103
  const scoring = DEFAULT_MEMORY_CONFIG.vectorStore.config.scoring!;
100
104
 
101
- it('should prioritize cosine similarity for factual memory (high alpha)', () => {
102
- const payload: MemoryPayload = { memoryId: 'mem-f1', userId: userId, type: 'factual', createdAt: twoDaysAgo };
105
+ it('should prioritize cosine similarity for assistant_preference (high alpha)', () => {
106
+ //
107
+ // assistant_preference: { alpha: 0.60, beta: 0.05, gamma: 0.35, halfLifeDays: Infinity }
108
+ const payload: MemoryPayload = { memoryId: 'mem-ap1', userId: userId, type: 'assistant_preference', createdAt: twoDaysAgo };
103
109
  const cosineScore = 0.9;
104
- const expectedRecency = (store as any).calculateRecencyScore(twoDaysAgo, scoring.factual.halfLifeDays);
105
- const expectedScore = scoring.factual.alpha * cosineScore + scoring.factual.beta * expectedRecency + scoring.factual.gamma;
110
+ const expectedRecency = 1.0; // Infinity halfLife → recency = 1
111
+ const expectedScore = scoring.assistant_preference.alpha * cosineScore + scoring.assistant_preference.beta * expectedRecency + scoring.assistant_preference.gamma;
106
112
  const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
107
113
  expect(hybridScore).toBeCloseTo(expectedScore, 5);
108
- expect(scoring.factual.alpha * cosineScore).toBeGreaterThan(scoring.factual.beta * expectedRecency);
114
+ // alpha * cosine (0.54) > beta * recency (0.05)
115
+ expect(scoring.assistant_preference.alpha * cosineScore).toBeGreaterThan(scoring.assistant_preference.beta * expectedRecency);
109
116
  });
110
117
 
111
- it('should prioritize recency for episodic memory (high beta, short half-life)', () => {
112
- const payload: MemoryPayload = { memoryId: 'mem-e1', userId: userId, type: 'episodic', createdAt: twoDaysAgo };
118
+ it('should return constant score for discussion (beta=1, alpha=0, gamma=0)', () => {
119
+ //
120
+ // discussion: { alpha: 0, beta: 1, gamma: 0, halfLifeDays: Infinity }
121
+ // Score = 0 * cosine + 1 * recency + 0 = 1 (constant)
122
+ const payload: MemoryPayload = { memoryId: 'mem-d1', userId: userId, type: 'discussion', createdAt: twoDaysAgo };
113
123
  const cosineScore = 0.5;
114
- const expectedRecency = (store as any).calculateRecencyScore(twoDaysAgo, scoring.episodic.halfLifeDays);
115
- const expectedScore = scoring.episodic.alpha * cosineScore + scoring.episodic.beta * expectedRecency + scoring.episodic.gamma;
124
+ const expectedRecency = 1.0; // Infinity halfLife → recency = 1
125
+ const expectedScore = scoring.discussion.alpha * cosineScore + scoring.discussion.beta * expectedRecency + scoring.discussion.gamma;
116
126
  const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
117
127
  expect(hybridScore).toBeCloseTo(expectedScore, 5);
118
- expect(scoring.episodic.beta * expectedRecency).toBeGreaterThan(0.2);
128
+ expect(hybridScore).toBe(1); // Score constant = 1
119
129
  });
120
130
 
121
- it('should have low score for old episodic memory', () => {
122
- const payload: MemoryPayload = { memoryId: 'mem-e2', userId: userId, type: 'episodic', createdAt: veryOldDate };
123
- const cosineScore = 0.9;
124
- const expectedRecency = (store as any).calculateRecencyScore(veryOldDate, scoring.episodic.halfLifeDays);
125
- const expectedScore = scoring.episodic.alpha * cosineScore + scoring.episodic.beta * expectedRecency + scoring.episodic.gamma;
126
- const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
127
- expect(expectedRecency).toBeLessThan(0.01);
128
- expect(hybridScore).toBeCloseTo(scoring.episodic.alpha * cosineScore + scoring.episodic.gamma, 5);
131
+ it('should have constant score for old discussion memory (no decay)', () => {
132
+ //
133
+ // discussion with Infinity halfLife → old memories have same score as new
134
+ const payload: MemoryPayload = { memoryId: 'mem-d2', userId: userId, type: 'discussion', createdAt: veryOldDate };
135
+ const cosineScore = 0.9;
136
+ const expectedRecency = 1.0; // Infinity halfLife → recency = 1 even for old dates
137
+ const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
138
+ expect(hybridScore).toBe(1); // Score constant = 1
129
139
  });
130
140
 
131
141
  it('should handle assistant_preference with no decay (Infinity half-life)', () => {
132
- const payload: MemoryPayload = { memoryId: 'mem-a1', userId: userId, type: 'assistant_preference', createdAt: veryOldDate };
133
- const cosineScore = 0.7;
134
- const expectedRecency = 1.0;
135
- const expectedScore = scoring.assistant_preference.alpha * cosineScore + scoring.assistant_preference.beta * expectedRecency + scoring.assistant_preference.gamma;
136
- const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
137
- expect(hybridScore).toBeCloseTo(expectedScore, 5);
142
+ const payload: MemoryPayload = { memoryId: 'mem-a1', userId: userId, type: 'assistant_preference', createdAt: veryOldDate };
143
+ const cosineScore = 0.7;
144
+ const expectedRecency = 1.0;
145
+ const expectedScore = scoring.assistant_preference.alpha * cosineScore + scoring.assistant_preference.beta * expectedRecency + scoring.assistant_preference.gamma;
146
+ const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
147
+ expect(hybridScore).toBeCloseTo(expectedScore, 5);
138
148
  });
139
149
 
140
150
  it('should use default scoring if type is missing', () => {
141
- const payload: MemoryPayload = { memoryId: 'mem-d1', userId: userId, createdAt: sevenDaysAgo }; // No type specified
142
- const cosineScore = 0.8;
143
- const expectedRecency = (store as any).calculateRecencyScore(sevenDaysAgo, scoring.default.halfLifeDays);
144
- const expectedScore = scoring.default.alpha * cosineScore + scoring.default.beta * expectedRecency + scoring.default.gamma;
145
- const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
146
- expect(hybridScore).toBeCloseTo(expectedScore, 5);
151
+ const payload: MemoryPayload = { memoryId: 'mem-def1', userId: userId, createdAt: sevenDaysAgo }; // No type specified
152
+ const cosineScore = 0.8;
153
+ const expectedRecency = (store as any).calculateRecencyScore(sevenDaysAgo, scoring.default.halfLifeDays);
154
+ const expectedScore = scoring.default.alpha * cosineScore + scoring.default.beta * expectedRecency + scoring.default.gamma;
155
+ const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
156
+ expect(hybridScore).toBeCloseTo(expectedScore, 5);
147
157
  });
148
158
 
149
159
  it('should return score >= 0 even with negative cosine similarity', () => {
150
- const payload: MemoryPayload = { memoryId: 'mem-s1', userId: userId, type: 'factual', createdAt: twoDaysAgo };
151
- const cosineScore = -0.5;
152
- const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
153
- expect(hybridScore).toBeGreaterThanOrEqual(0);
160
+ const payload: MemoryPayload = { memoryId: 'mem-neg1', userId: userId, type: 'assistant_preference', createdAt: twoDaysAgo };
161
+ const cosineScore = -0.5;
162
+ const hybridScore = (store as any).calculateHybridScore(cosineScore, payload);
163
+ expect(hybridScore).toBeGreaterThanOrEqual(0);
154
164
  });
155
165
 
156
166
  });