@lobehub/lobehub 2.0.0-next.110 → 2.0.0-next.112

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 (40) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/changelog/v1.json +18 -0
  3. package/docs/development/database-schema.dbml +2 -1
  4. package/package.json +1 -1
  5. package/packages/context-engine/src/index.ts +1 -1
  6. package/packages/context-engine/src/providers/KnowledgeInjector.ts +78 -0
  7. package/packages/context-engine/src/providers/index.ts +2 -0
  8. package/packages/database/migrations/0047_add_slug_document.sql +1 -5
  9. package/packages/database/migrations/meta/0047_snapshot.json +30 -14
  10. package/packages/database/migrations/meta/_journal.json +1 -1
  11. package/packages/database/src/core/migrations.json +3 -3
  12. package/packages/database/src/models/__tests__/agent.test.ts +172 -3
  13. package/packages/database/src/models/__tests__/userMemories.test.ts +1382 -0
  14. package/packages/database/src/models/agent.ts +22 -1
  15. package/packages/database/src/models/userMemory.ts +993 -0
  16. package/packages/database/src/schemas/file.ts +5 -10
  17. package/packages/database/src/schemas/userMemories.ts +22 -5
  18. package/packages/model-bank/src/aiModels/qwen.ts +41 -3
  19. package/packages/model-runtime/src/providers/qwen/index.ts +9 -3
  20. package/packages/prompts/src/prompts/files/__snapshots__/knowledgeBase.test.ts.snap +103 -0
  21. package/packages/prompts/src/prompts/files/index.ts +3 -0
  22. package/packages/prompts/src/prompts/files/knowledgeBase.test.ts +167 -0
  23. package/packages/prompts/src/prompts/files/knowledgeBase.ts +85 -0
  24. package/packages/types/src/files/index.ts +1 -0
  25. package/packages/types/src/index.ts +1 -0
  26. package/packages/types/src/knowledgeBase/index.ts +1 -0
  27. package/packages/types/src/userMemory/index.ts +3 -0
  28. package/packages/types/src/userMemory/layers.ts +54 -0
  29. package/packages/types/src/userMemory/shared.ts +64 -0
  30. package/packages/types/src/userMemory/tools.ts +240 -0
  31. package/src/features/ChatList/Messages/index.tsx +16 -19
  32. package/src/features/ChatList/components/ContextMenu.tsx +23 -16
  33. package/src/helpers/toolEngineering/index.ts +5 -9
  34. package/src/hooks/useQueryParam.ts +24 -22
  35. package/src/server/routers/async/file.ts +2 -7
  36. package/src/server/routers/lambda/chunk.ts +6 -1
  37. package/src/services/chat/contextEngineering.ts +19 -0
  38. package/src/store/agent/slices/chat/selectors/agent.ts +4 -0
  39. package/src/store/chat/slices/builtinTool/actions/knowledgeBase.ts +5 -16
  40. package/src/tools/knowledge-base/ExecutionRuntime/index.ts +3 -3
@@ -0,0 +1,1382 @@
1
+ // @vitest-environment node
2
+ import { eq } from 'drizzle-orm';
3
+ import { nanoid } from 'nanoid';
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
5
+
6
+ import { idGenerator } from '@/database/utils/idGenerator';
7
+ import { LayersEnum, MergeStrategyEnum, TypesEnum } from '@/types/userMemory';
8
+
9
+ import {
10
+ userMemories,
11
+ userMemoriesContexts,
12
+ userMemoriesExperiences,
13
+ userMemoriesIdentities,
14
+ userMemoriesPreferences,
15
+ users,
16
+ } from '../../schemas';
17
+ import { LobeChatDatabase } from '../../type';
18
+ import {
19
+ BaseCreateUserMemoryParams,
20
+ CreateUserMemoryContextParams,
21
+ CreateUserMemoryExperienceParams,
22
+ CreateUserMemoryIdentityParams,
23
+ CreateUserMemoryPreferenceParams,
24
+ UserMemoryModel,
25
+ } from '../userMemory';
26
+ import { getTestDB } from './_util';
27
+
28
+ const serverDB: LobeChatDatabase = await getTestDB();
29
+
30
+ const userId = idGenerator('user');
31
+ const userId2 = idGenerator('user');
32
+ const userMemoryModel = new UserMemoryModel(serverDB, userId);
33
+
34
+ /**
35
+ * Generate a random normalized embedding vector
36
+ * @param dimensions - Vector dimensions (default: 1024)
37
+ * @returns Normalized random vector
38
+ */
39
+ function generateRandomEmbedding(dimensions: number = 1024): number[] {
40
+ const vector = Array(dimensions)
41
+ .fill(0)
42
+ .map(() => Math.random() * 2 - 1); // Random values between -1 and 1
43
+
44
+ // Normalize the vector
45
+ const magnitude = Math.sqrt(vector.reduce((sum, val) => sum + val * val, 0));
46
+ return vector.map((val) => val / magnitude);
47
+ }
48
+
49
+ const mockEmbedding = generateRandomEmbedding();
50
+ const mockEmbedding2 = generateRandomEmbedding();
51
+
52
+ function createAxisVector(dimensions: number = 1024, index: number = 0): number[] {
53
+ return Array.from({ length: dimensions }, (_, i) => (i === index ? 1 : 0));
54
+ }
55
+
56
+ // Assert that two numeric vectors are equal within a precision tolerance
57
+ function expectVectorToBeClose(
58
+ actual: number[] | null | undefined,
59
+ expected: number[],
60
+ precision: number = 5,
61
+ ) {
62
+ expect(actual).toBeDefined();
63
+ expect(actual).not.toBeNull();
64
+
65
+ const actualVector = actual as number[];
66
+
67
+ expect(actualVector.length).toBe(expected.length);
68
+ actualVector.forEach((value, index) => {
69
+ expect(value).toBeCloseTo(expected[index]!, precision);
70
+ });
71
+ }
72
+
73
+ beforeEach(async () => {
74
+ await serverDB.delete(users);
75
+ await serverDB.insert(users).values([{ id: userId }, { id: userId2 }]);
76
+ });
77
+
78
+ afterEach(async () => {
79
+ await serverDB.delete(users).where(eq(users.id, userId));
80
+ });
81
+
82
+ function generateRandomCreateUserMemoryParams(
83
+ memoryLayer: BaseCreateUserMemoryParams['memoryLayer'],
84
+ ): BaseCreateUserMemoryParams {
85
+ return {
86
+ title: 'title ' + nanoid(),
87
+ summary: 'summary ' + nanoid(),
88
+ summaryEmbedding: generateRandomEmbedding(),
89
+ memoryType: TypesEnum.Activity,
90
+ memoryCategory: 'category',
91
+ memoryLayer,
92
+ details: 'details ' + nanoid(),
93
+ } satisfies BaseCreateUserMemoryParams;
94
+ }
95
+
96
+ function generateRandomCreateUserMemoryExperienceParams() {
97
+ return {
98
+ ...generateRandomCreateUserMemoryParams(LayersEnum.Experience),
99
+ experience: {
100
+ action: 'action ' + nanoid(),
101
+ actionVector: generateRandomEmbedding(),
102
+ keyLearning: 'keyLearning ' + nanoid(),
103
+ keyLearningVector: generateRandomEmbedding(),
104
+ metadata: {},
105
+ possibleOutcome: 'possibleOutcome ' + nanoid(),
106
+ reasoning: 'reasoning ' + nanoid(),
107
+ scoreConfidence: 0.5,
108
+ situation: 'situation ' + nanoid(),
109
+ situationVector: generateRandomEmbedding(),
110
+ tags: [],
111
+ type: 'learning',
112
+ },
113
+ } as CreateUserMemoryExperienceParams;
114
+ }
115
+
116
+ function generateRandomCreateUserMemoryIdentityParams() {
117
+ const relationshipOptions = ['self', 'friend', 'colleague', 'other'];
118
+
119
+ return {
120
+ ...generateRandomCreateUserMemoryParams(LayersEnum.Identity),
121
+ identity: {
122
+ description: 'identity description ' + nanoid(),
123
+ descriptionVector: generateRandomEmbedding(),
124
+ episodicDate: new Date(),
125
+ metadata: {},
126
+ relationship: relationshipOptions[Math.floor(Math.random() * relationshipOptions.length)],
127
+ role: 'role ' + nanoid(),
128
+ tags: [],
129
+ type: 'personal',
130
+ },
131
+ } as CreateUserMemoryIdentityParams;
132
+ }
133
+
134
+ function generateRandomCreateUserMemoryContextParams() {
135
+ return {
136
+ ...generateRandomCreateUserMemoryParams(LayersEnum.Context),
137
+ context: {
138
+ associatedObjects: [],
139
+ associatedSubjects: [],
140
+ currentStatus: 'status ' + nanoid(),
141
+ description: 'context description ' + nanoid(),
142
+ descriptionVector: generateRandomEmbedding(),
143
+ metadata: {},
144
+ scoreImpact: 0.5,
145
+ scoreUrgency: 0.4,
146
+ tags: [],
147
+ title: 'context title ' + nanoid(),
148
+ titleVector: generateRandomEmbedding(),
149
+ type: 'environment',
150
+ userMemoryIds: [],
151
+ },
152
+ } as CreateUserMemoryContextParams;
153
+ }
154
+
155
+ function generateRandomCreateUserMemoryPreferenceParams() {
156
+ return {
157
+ ...generateRandomCreateUserMemoryParams(LayersEnum.Preference),
158
+ preference: {
159
+ conclusionDirectives: 'directive ' + nanoid(),
160
+ conclusionDirectivesVector: generateRandomEmbedding(),
161
+ metadata: {},
162
+ scorePriority: 0.7,
163
+ suggestions: 'suggestions ' + nanoid(),
164
+ tags: [],
165
+ type: 'choice',
166
+ },
167
+ } as CreateUserMemoryPreferenceParams;
168
+ }
169
+
170
+ describe('UserMemoryModel', () => {
171
+ describe('create layered memories', () => {
172
+ it('creates a context memory and links the base record', async () => {
173
+ const params = generateRandomCreateUserMemoryContextParams();
174
+ params.context.metadata = { contextMeta: true };
175
+ params.context.tags = ['context-tag'];
176
+
177
+ const { context, memory } = await userMemoryModel.createContextMemory(params);
178
+
179
+ const persistedMemory = await serverDB.query.userMemories.findFirst({
180
+ where: eq(userMemories.id, memory.id),
181
+ });
182
+ const persistedContext = await serverDB.query.userMemoriesContexts.findFirst({
183
+ where: eq(userMemoriesContexts.id, context.id),
184
+ });
185
+
186
+ expect(persistedMemory?.metadata).toEqual({ contextMeta: true });
187
+ expect(persistedMemory?.tags).toEqual(['context-tag']);
188
+ expect(persistedContext?.userMemoryIds).toEqual([memory.id]);
189
+ expect(persistedContext?.metadata).toEqual({ contextMeta: true });
190
+ expect(persistedContext?.tags).toEqual(['context-tag']);
191
+ });
192
+
193
+ it('creates an experience memory and stores vectors and fallbacks', async () => {
194
+ const params = generateRandomCreateUserMemoryExperienceParams();
195
+ params.experience.metadata = { expMeta: 1 };
196
+ params.experience.tags = ['exp-tag'];
197
+ params.experience.type = null;
198
+
199
+ const { experience, memory } = await userMemoryModel.createExperienceMemory(params);
200
+
201
+ const persistedMemory = await serverDB.query.userMemories.findFirst({
202
+ where: eq(userMemories.id, memory.id),
203
+ });
204
+ const persistedExperience = await serverDB.query.userMemoriesExperiences.findFirst({
205
+ where: eq(userMemoriesExperiences.id, experience.id),
206
+ });
207
+
208
+ expect(persistedMemory?.metadata).toEqual({ expMeta: 1 });
209
+ expect(persistedMemory?.tags).toEqual(['exp-tag']);
210
+ expect(persistedExperience?.userMemoryId).toBe(memory.id);
211
+ expect(persistedExperience?.type).toBe(params.memoryType);
212
+ expectVectorToBeClose(persistedExperience?.actionVector, params.experience.actionVector!);
213
+ expectVectorToBeClose(
214
+ persistedExperience?.situationVector,
215
+ params.experience.situationVector!,
216
+ );
217
+ });
218
+
219
+ it('creates a preference memory and links to the base record', async () => {
220
+ const params = generateRandomCreateUserMemoryPreferenceParams();
221
+ params.preference.metadata = { prefMeta: 'yes' };
222
+ params.preference.tags = ['pref-tag'];
223
+ params.preference.type = null;
224
+
225
+ const { preference, memory } = await userMemoryModel.createPreferenceMemory(params);
226
+
227
+ const persistedMemory = await serverDB.query.userMemories.findFirst({
228
+ where: eq(userMemories.id, memory.id),
229
+ });
230
+ const persistedPreference = await serverDB.query.userMemoriesPreferences.findFirst({
231
+ where: eq(userMemoriesPreferences.id, preference.id),
232
+ });
233
+
234
+ expect(persistedMemory?.metadata).toEqual({ prefMeta: 'yes' });
235
+ expect(persistedMemory?.tags).toEqual(['pref-tag']);
236
+ expect(persistedPreference?.userMemoryId).toBe(memory.id);
237
+ expect(persistedPreference?.type).toBe(params.memoryType);
238
+ expectVectorToBeClose(
239
+ persistedPreference?.conclusionDirectivesVector,
240
+ params.preference.conclusionDirectivesVector!,
241
+ );
242
+ });
243
+ });
244
+
245
+ describe('delete', () => {
246
+ it('should delete a memory by id', async () => {
247
+ const created = await userMemoryModel.create(
248
+ generateRandomCreateUserMemoryExperienceParams(),
249
+ );
250
+
251
+ await userMemoryModel.delete(created.id);
252
+
253
+ const deleted = await serverDB.query.userMemories.findFirst({
254
+ where: eq(userMemories.id, created.id),
255
+ });
256
+
257
+ expect(deleted).toBeUndefined();
258
+ });
259
+
260
+ it('should only delete own user memories', async () => {
261
+ const anotherUserModel = new UserMemoryModel(serverDB, userId2);
262
+ const created = await anotherUserModel.create(
263
+ generateRandomCreateUserMemoryExperienceParams(),
264
+ );
265
+
266
+ await userMemoryModel.delete(created.id);
267
+
268
+ const stillExists = await serverDB.query.userMemories.findFirst({
269
+ where: eq(userMemories.id, created.id),
270
+ });
271
+
272
+ expect(stillExists).toBeDefined();
273
+ });
274
+ });
275
+
276
+ describe('deleteAll', () => {
277
+ it('should delete all memories for the user', async () => {
278
+ await userMemoryModel.create(generateRandomCreateUserMemoryExperienceParams());
279
+ await userMemoryModel.create(generateRandomCreateUserMemoryIdentityParams());
280
+ await userMemoryModel.create(generateRandomCreateUserMemoryContextParams());
281
+ await userMemoryModel.create(generateRandomCreateUserMemoryPreferenceParams());
282
+ await userMemoryModel.deleteAll();
283
+
284
+ const remaining = await serverDB.query.userMemories.findMany({
285
+ where: eq(userMemories.userId, userId),
286
+ });
287
+
288
+ expect(remaining).toHaveLength(0);
289
+ });
290
+
291
+ it('should only delete memories for the user, not others', async () => {
292
+ // user 2
293
+ const user2Memory = await userMemoryModel.create(
294
+ generateRandomCreateUserMemoryExperienceParams(),
295
+ );
296
+ await serverDB
297
+ .update(userMemories)
298
+ .set({ userId: userId2 })
299
+ .where(eq(userMemories.id, user2Memory.id));
300
+
301
+ await userMemoryModel.create(generateRandomCreateUserMemoryIdentityParams());
302
+ await userMemoryModel.create(generateRandomCreateUserMemoryContextParams());
303
+ await userMemoryModel.create(generateRandomCreateUserMemoryPreferenceParams());
304
+ await userMemoryModel.deleteAll();
305
+
306
+ const user1Remaining = await serverDB.query.userMemories.findMany({
307
+ where: eq(userMemories.userId, userId),
308
+ });
309
+ const user2Remaining = await serverDB.query.userMemories.findMany({
310
+ where: eq(userMemories.userId, userId2),
311
+ });
312
+
313
+ expect(user1Remaining).toHaveLength(0);
314
+ expect(user2Remaining).toHaveLength(1);
315
+ });
316
+ });
317
+
318
+ describe('search', () => {
319
+ it('should return layered results with split limits', async () => {
320
+ const experienceMemoryOne = await userMemoryModel.create(
321
+ generateRandomCreateUserMemoryExperienceParams(),
322
+ );
323
+ await serverDB.insert(userMemoriesExperiences).values({
324
+ ...generateRandomCreateUserMemoryExperienceParams().experience,
325
+ userId,
326
+ userMemoryId: experienceMemoryOne.id,
327
+ });
328
+
329
+ const experienceMemoryTwo = await userMemoryModel.create(
330
+ generateRandomCreateUserMemoryExperienceParams(),
331
+ );
332
+ await serverDB.insert(userMemoriesExperiences).values({
333
+ ...generateRandomCreateUserMemoryExperienceParams().experience,
334
+ userId,
335
+ userMemoryId: experienceMemoryTwo.id,
336
+ });
337
+
338
+ const preferenceMemoryOne = await userMemoryModel.create(
339
+ generateRandomCreateUserMemoryPreferenceParams(),
340
+ );
341
+ await serverDB.insert(userMemoriesPreferences).values({
342
+ ...generateRandomCreateUserMemoryPreferenceParams().preference,
343
+ userId,
344
+ userMemoryId: preferenceMemoryOne.id,
345
+ });
346
+
347
+ const preferenceMemoryTwo = await userMemoryModel.create(
348
+ generateRandomCreateUserMemoryPreferenceParams(),
349
+ );
350
+ await serverDB.insert(userMemoriesPreferences).values({
351
+ ...generateRandomCreateUserMemoryPreferenceParams().preference,
352
+ userId,
353
+ userMemoryId: preferenceMemoryTwo.id,
354
+ });
355
+
356
+ await serverDB.insert(userMemoriesContexts).values({
357
+ ...generateRandomCreateUserMemoryContextParams().context,
358
+ userId,
359
+ userMemoryIds: [experienceMemoryOne.id, preferenceMemoryOne.id],
360
+ });
361
+ await serverDB.insert(userMemoriesContexts).values({
362
+ ...generateRandomCreateUserMemoryContextParams().context,
363
+ userId,
364
+ userMemoryIds: [experienceMemoryTwo.id, preferenceMemoryTwo.id],
365
+ });
366
+
367
+ const result = await userMemoryModel.search({
368
+ limit: 5,
369
+ limits: {
370
+ contexts: 1,
371
+ experiences: 1,
372
+ preferences: 1,
373
+ },
374
+ });
375
+
376
+ expect(result.experiences).toHaveLength(1);
377
+ expect(result.preferences).toHaveLength(1);
378
+ expect(result.contexts).toHaveLength(1);
379
+ });
380
+
381
+ it('ranks contexts, experiences, and preferences by embedding similarity', async () => {
382
+ const now = new Date('2024-01-01T00:00:00.000Z');
383
+ const axis0 = createAxisVector();
384
+ const axis1 = createAxisVector(1024, 1);
385
+
386
+ const closeContextId = idGenerator('memory');
387
+ const farContextId = idGenerator('memory');
388
+ await serverDB.insert(userMemoriesContexts).values([
389
+ {
390
+ accessedAt: now,
391
+ createdAt: now,
392
+ description: 'close context',
393
+ descriptionVector: axis0,
394
+ id: closeContextId,
395
+ updatedAt: now,
396
+ userId,
397
+ },
398
+ {
399
+ accessedAt: now,
400
+ createdAt: now,
401
+ description: 'far context',
402
+ descriptionVector: axis1,
403
+ id: farContextId,
404
+ updatedAt: now,
405
+ userId,
406
+ },
407
+ ]);
408
+
409
+ const closeExperienceMemoryId = idGenerator('memory');
410
+ const farExperienceMemoryId = idGenerator('memory');
411
+ await serverDB.insert(userMemories).values([
412
+ {
413
+ accessedAt: now,
414
+ createdAt: now,
415
+ id: closeExperienceMemoryId,
416
+ lastAccessedAt: now,
417
+ memoryLayer: 'experience',
418
+ summary: 'close experience base',
419
+ updatedAt: now,
420
+ userId,
421
+ },
422
+ {
423
+ accessedAt: now,
424
+ createdAt: now,
425
+ id: farExperienceMemoryId,
426
+ lastAccessedAt: now,
427
+ memoryLayer: 'experience',
428
+ summary: 'far experience base',
429
+ updatedAt: now,
430
+ userId,
431
+ },
432
+ ]);
433
+
434
+ const closeExperienceId = idGenerator('memory');
435
+ const farExperienceId = idGenerator('memory');
436
+ await serverDB.insert(userMemoriesExperiences).values([
437
+ {
438
+ accessedAt: now,
439
+ action: 'close action',
440
+ actionVector: axis0,
441
+ createdAt: now,
442
+ id: closeExperienceId,
443
+ situation: 'close situation',
444
+ situationVector: axis0,
445
+ updatedAt: now,
446
+ userId,
447
+ userMemoryId: closeExperienceMemoryId,
448
+ },
449
+ {
450
+ accessedAt: now,
451
+ action: 'far action',
452
+ actionVector: axis1,
453
+ createdAt: now,
454
+ id: farExperienceId,
455
+ situation: 'far situation',
456
+ situationVector: axis1,
457
+ updatedAt: now,
458
+ userId,
459
+ userMemoryId: farExperienceMemoryId,
460
+ },
461
+ ]);
462
+
463
+ const closePreferenceMemoryId = idGenerator('memory');
464
+ const farPreferenceMemoryId = idGenerator('memory');
465
+ await serverDB.insert(userMemories).values([
466
+ {
467
+ accessedAt: now,
468
+ createdAt: now,
469
+ id: closePreferenceMemoryId,
470
+ lastAccessedAt: now,
471
+ memoryLayer: 'preference',
472
+ summary: 'close preference base',
473
+ updatedAt: now,
474
+ userId,
475
+ },
476
+ {
477
+ accessedAt: now,
478
+ createdAt: now,
479
+ id: farPreferenceMemoryId,
480
+ lastAccessedAt: now,
481
+ memoryLayer: 'preference',
482
+ summary: 'far preference base',
483
+ updatedAt: now,
484
+ userId,
485
+ },
486
+ ]);
487
+
488
+ const closePreferenceId = idGenerator('memory');
489
+ const farPreferenceId = idGenerator('memory');
490
+ await serverDB.insert(userMemoriesPreferences).values([
491
+ {
492
+ accessedAt: now,
493
+ conclusionDirectives: 'close preference',
494
+ conclusionDirectivesVector: axis0,
495
+ createdAt: now,
496
+ id: closePreferenceId,
497
+ updatedAt: now,
498
+ userId,
499
+ userMemoryId: closePreferenceMemoryId,
500
+ },
501
+ {
502
+ accessedAt: now,
503
+ conclusionDirectives: 'far preference',
504
+ conclusionDirectivesVector: axis1,
505
+ createdAt: now,
506
+ id: farPreferenceId,
507
+ updatedAt: now,
508
+ userId,
509
+ userMemoryId: farPreferenceMemoryId,
510
+ },
511
+ ]);
512
+
513
+ const result = await userMemoryModel.searchWithEmbedding({
514
+ embedding: axis0,
515
+ limits: {
516
+ contexts: 2,
517
+ experiences: 2,
518
+ preferences: 2,
519
+ },
520
+ });
521
+
522
+ expect(result.contexts[0]?.id).toBe(closeContextId);
523
+ expect(result.experiences[0]?.id).toBe(closeExperienceId);
524
+ expect(result.preferences[0]?.id).toBe(closePreferenceId);
525
+ });
526
+ });
527
+
528
+ describe('findById', () => {
529
+ it('returns own memories and updates access metrics', async () => {
530
+ const created = await userMemoryModel.create(
531
+ generateRandomCreateUserMemoryExperienceParams(),
532
+ );
533
+
534
+ const before = await serverDB.query.userMemories.findFirst({
535
+ where: eq(userMemories.id, created.id),
536
+ });
537
+
538
+ expect(before).toBeDefined();
539
+ expect(before?.accessedCount).toBe(0);
540
+
541
+ const found = await userMemoryModel.findById(created.id);
542
+
543
+ expect(found).toBeDefined();
544
+ expect(found?.id).toBe(created.id);
545
+
546
+ const after = await serverDB.query.userMemories.findFirst({
547
+ where: eq(userMemories.id, created.id),
548
+ });
549
+
550
+ expect(after?.accessedCount).toBe((before?.accessedCount ?? 0) + 1);
551
+ expect(after?.lastAccessedAt.getTime()).toBeGreaterThanOrEqual(
552
+ before!.lastAccessedAt.getTime(),
553
+ );
554
+ });
555
+
556
+ it('does not expose or mutate memories belonging to other users', async () => {
557
+ const anotherUserModel = new UserMemoryModel(serverDB, userId2);
558
+ const otherMemory = await anotherUserModel.create(
559
+ generateRandomCreateUserMemoryExperienceParams(),
560
+ );
561
+
562
+ const found = await userMemoryModel.findById(otherMemory.id);
563
+
564
+ expect(found).toBeUndefined();
565
+
566
+ const persisted = await serverDB.query.userMemories.findFirst({
567
+ where: eq(userMemories.id, otherMemory.id),
568
+ });
569
+
570
+ expect(persisted?.accessedCount).toBe(0);
571
+ });
572
+ });
573
+
574
+ describe('update', () => {
575
+ it('updates mutable fields for own memories only', async () => {
576
+ const created = await userMemoryModel.create(
577
+ generateRandomCreateUserMemoryExperienceParams(),
578
+ );
579
+
580
+ await userMemoryModel.update(created.id, {
581
+ memoryCategory: 'updated-category',
582
+ summary: 'Updated summary',
583
+ title: 'Updated title',
584
+ });
585
+
586
+ const updated = await serverDB.query.userMemories.findFirst({
587
+ where: eq(userMemories.id, created.id),
588
+ });
589
+
590
+ expect(updated?.memoryCategory).toBe('updated-category');
591
+ expect(updated?.summary).toBe('Updated summary');
592
+ expect(updated?.title).toBe('Updated title');
593
+ expect(updated?.updatedAt.getTime()).toBeGreaterThan(created.createdAt.getTime());
594
+ });
595
+
596
+ it('ignores updates for memories owned by other users', async () => {
597
+ const anotherUserModel = new UserMemoryModel(serverDB, userId2);
598
+ const otherMemory = await anotherUserModel.create(
599
+ generateRandomCreateUserMemoryExperienceParams(),
600
+ );
601
+
602
+ await userMemoryModel.update(otherMemory.id, {
603
+ summary: 'Should not update',
604
+ });
605
+
606
+ const persisted = await serverDB.query.userMemories.findFirst({
607
+ where: eq(userMemories.id, otherMemory.id),
608
+ });
609
+
610
+ expect(persisted?.summary).not.toBe('Should not update');
611
+ });
612
+ });
613
+
614
+ describe('update vector methods', () => {
615
+ it('updates base user memory vectors', async () => {
616
+ const memoryId = idGenerator('memory');
617
+ await serverDB.insert(userMemories).values({
618
+ details: 'details',
619
+ id: memoryId,
620
+ lastAccessedAt: new Date(),
621
+ summary: 'summary',
622
+ title: 'title',
623
+ userId,
624
+ });
625
+
626
+ const summaryVector = generateRandomEmbedding();
627
+ const detailsVector = generateRandomEmbedding();
628
+
629
+ await userMemoryModel.updateUserMemoryVectors(memoryId, {
630
+ detailsVector1024: detailsVector,
631
+ summaryVector1024: summaryVector,
632
+ });
633
+
634
+ const updated = await serverDB.query.userMemories.findFirst({
635
+ where: eq(userMemories.id, memoryId),
636
+ });
637
+
638
+ expectVectorToBeClose(updated?.summaryVector1024, summaryVector);
639
+ expectVectorToBeClose(updated?.detailsVector1024, detailsVector);
640
+ });
641
+
642
+ it('ignores user memory vector updates when payload is empty', async () => {
643
+ const memoryId = idGenerator('memory');
644
+ const timestamp = new Date('2024-01-01T00:00:00.000Z');
645
+ const summaryVector = generateRandomEmbedding();
646
+
647
+ await serverDB.insert(userMemories).values({
648
+ accessedAt: timestamp,
649
+ createdAt: timestamp,
650
+ details: 'details',
651
+ id: memoryId,
652
+ lastAccessedAt: timestamp,
653
+ summary: 'summary',
654
+ summaryVector1024: summaryVector,
655
+ updatedAt: timestamp,
656
+ userId,
657
+ });
658
+
659
+ const before = await serverDB.query.userMemories.findFirst({
660
+ where: eq(userMemories.id, memoryId),
661
+ });
662
+
663
+ await userMemoryModel.updateUserMemoryVectors(memoryId, {});
664
+
665
+ const after = await serverDB.query.userMemories.findFirst({
666
+ where: eq(userMemories.id, memoryId),
667
+ });
668
+
669
+ expect(after?.updatedAt.getTime()).toBe(before?.updatedAt.getTime());
670
+ expectVectorToBeClose(after?.summaryVector1024, summaryVector);
671
+ });
672
+
673
+ it('updates context vectors', async () => {
674
+ const contextId = idGenerator('memory');
675
+ await serverDB.insert(userMemoriesContexts).values({
676
+ description: 'desc',
677
+ id: contextId,
678
+ title: 'title',
679
+ userId,
680
+ });
681
+
682
+ const descriptionVector = generateRandomEmbedding();
683
+ await userMemoryModel.updateContextVectors(contextId, { descriptionVector });
684
+ const updated = await serverDB.query.userMemoriesContexts.findFirst({
685
+ where: eq(userMemoriesContexts.id, contextId),
686
+ });
687
+
688
+ expectVectorToBeClose(updated?.descriptionVector, descriptionVector);
689
+ });
690
+
691
+ it('updates preference vector values including null assignments', async () => {
692
+ const preferenceId = idGenerator('memory');
693
+ await serverDB.insert(userMemoriesPreferences).values({
694
+ conclusionDirectives: 'directive',
695
+ conclusionDirectivesVector: generateRandomEmbedding(),
696
+ id: preferenceId,
697
+ userId,
698
+ });
699
+
700
+ await userMemoryModel.updatePreferenceVectors(preferenceId, {
701
+ conclusionDirectivesVector: null,
702
+ });
703
+
704
+ const updated = await serverDB.query.userMemoriesPreferences.findFirst({
705
+ where: eq(userMemoriesPreferences.id, preferenceId),
706
+ });
707
+
708
+ expect(updated?.conclusionDirectivesVector).toBeNull();
709
+ });
710
+
711
+ it('updates identity vectors', async () => {
712
+ const identityId = idGenerator('memory');
713
+ await serverDB.insert(userMemoriesIdentities).values({
714
+ description: 'identity',
715
+ id: identityId,
716
+ userId,
717
+ });
718
+
719
+ const descriptionVector = generateRandomEmbedding();
720
+
721
+ await userMemoryModel.updateIdentityVectors(identityId, {
722
+ descriptionVector,
723
+ });
724
+
725
+ const updated = await serverDB.query.userMemoriesIdentities.findFirst({
726
+ where: eq(userMemoriesIdentities.id, identityId),
727
+ });
728
+
729
+ expectVectorToBeClose(updated?.descriptionVector, descriptionVector);
730
+ });
731
+
732
+ it('skips identity vector updates when no fields are provided', async () => {
733
+ const identityId = idGenerator('memory');
734
+ const timestamp = new Date('2024-03-01T00:00:00.000Z');
735
+ const descriptionVector = generateRandomEmbedding();
736
+
737
+ await serverDB.insert(userMemoriesIdentities).values({
738
+ accessedAt: timestamp,
739
+ createdAt: timestamp,
740
+ description: 'identity',
741
+ descriptionVector,
742
+ id: identityId,
743
+ updatedAt: timestamp,
744
+ userId,
745
+ });
746
+
747
+ await userMemoryModel.updateIdentityVectors(identityId, {});
748
+
749
+ const persisted = await serverDB.query.userMemoriesIdentities.findFirst({
750
+ where: eq(userMemoriesIdentities.id, identityId),
751
+ });
752
+
753
+ expect(persisted?.updatedAt.getTime()).toBe(timestamp.getTime());
754
+ expectVectorToBeClose(persisted?.descriptionVector, descriptionVector);
755
+ });
756
+
757
+ it('updates experience vectors without touching unspecified fields', async () => {
758
+ const experienceId = idGenerator('memory');
759
+ const originalSituationVector = generateRandomEmbedding();
760
+ const originalKeyLearningVector = generateRandomEmbedding();
761
+ const originalActionVector = generateRandomEmbedding();
762
+
763
+ await serverDB.insert(userMemoriesExperiences).values({
764
+ action: 'action',
765
+ actionVector: originalActionVector,
766
+ id: experienceId,
767
+ keyLearning: 'key learning',
768
+ keyLearningVector: originalKeyLearningVector,
769
+ situation: 'situation',
770
+ situationVector: originalSituationVector,
771
+ userId,
772
+ });
773
+
774
+ const updatedActionVector = generateRandomEmbedding();
775
+
776
+ await userMemoryModel.updateExperienceVectors(experienceId, {
777
+ actionVector: updatedActionVector,
778
+ });
779
+
780
+ const updated = await serverDB.query.userMemoriesExperiences.findFirst({
781
+ where: eq(userMemoriesExperiences.id, experienceId),
782
+ });
783
+
784
+ expectVectorToBeClose(updated?.actionVector, updatedActionVector);
785
+ expectVectorToBeClose(updated?.situationVector, originalSituationVector);
786
+ expectVectorToBeClose(updated?.keyLearningVector, originalKeyLearningVector);
787
+ });
788
+
789
+ it('updates multiple experience vector fields and supports null assignments', async () => {
790
+ const experienceId = idGenerator('memory');
791
+ const originalSituationVector = generateRandomEmbedding();
792
+ const originalKeyLearningVector = generateRandomEmbedding();
793
+ const originalActionVector = generateRandomEmbedding();
794
+
795
+ await serverDB.insert(userMemoriesExperiences).values({
796
+ action: 'action',
797
+ actionVector: originalActionVector,
798
+ id: experienceId,
799
+ keyLearning: 'key learning',
800
+ keyLearningVector: originalKeyLearningVector,
801
+ situation: 'situation',
802
+ situationVector: originalSituationVector,
803
+ userId,
804
+ });
805
+
806
+ const newSituationVector = generateRandomEmbedding();
807
+
808
+ await userMemoryModel.updateExperienceVectors(experienceId, {
809
+ keyLearningVector: null,
810
+ situationVector: newSituationVector,
811
+ });
812
+
813
+ const updated = await serverDB.query.userMemoriesExperiences.findFirst({
814
+ where: eq(userMemoriesExperiences.id, experienceId),
815
+ });
816
+
817
+ expectVectorToBeClose(updated?.situationVector, newSituationVector);
818
+ expect(updated?.keyLearningVector).toBeNull();
819
+ expectVectorToBeClose(updated?.actionVector, originalActionVector);
820
+ });
821
+ });
822
+
823
+ describe('identity entry operations', () => {
824
+ it('adds a new identity entry with base memory', async () => {
825
+ const summaryVector = generateRandomEmbedding();
826
+ const descriptionVector = generateRandomEmbedding();
827
+
828
+ const { identityId, userMemoryId } = await userMemoryModel.addIdentityEntry({
829
+ base: {
830
+ memoryCategory: 'profile',
831
+ memoryLayer: 'identity',
832
+ memoryType: 'personal-profile',
833
+ metadata: { meta: 'value' },
834
+ summary: 'Initial summary',
835
+ summaryVector1024: summaryVector,
836
+ title: 'Identity Summary',
837
+ },
838
+ identity: {
839
+ description: 'A detailed description of the user identity',
840
+ descriptionVector,
841
+ episodicDate: new Date('2024-01-01T00:00:00.000Z'),
842
+ metadata: { extracted: ['friend'] },
843
+ relationship: 'friend',
844
+ role: 'software engineer',
845
+ tags: ['tag-a'],
846
+ type: 'personal',
847
+ },
848
+ });
849
+
850
+ const baseMemory = await serverDB.query.userMemories.findFirst({
851
+ where: eq(userMemories.id, userMemoryId),
852
+ });
853
+ const identityMemory = await serverDB.query.userMemoriesIdentities.findFirst({
854
+ where: eq(userMemoriesIdentities.id, identityId),
855
+ });
856
+
857
+ expect(baseMemory?.summary).toBe('Initial summary');
858
+ expectVectorToBeClose(baseMemory?.summaryVector1024, summaryVector);
859
+ expect(baseMemory?.memoryLayer).toBe('identity');
860
+ expect(baseMemory?.metadata).toEqual({ meta: 'value' });
861
+
862
+ expect(identityMemory?.description).toBe('A detailed description of the user identity');
863
+ expectVectorToBeClose(identityMemory?.descriptionVector, descriptionVector);
864
+ expect(identityMemory?.relationship).toBe('friend');
865
+ expect(identityMemory?.role).toBe('software engineer');
866
+ expect(identityMemory?.type).toBe('personal');
867
+ expect(identityMemory?.userMemoryId).toBe(userMemoryId);
868
+ });
869
+
870
+ it('normalizes identity fields and coerces date inputs when adding entries', async () => {
871
+ const baseDate = '2024-02-02T12:00:00.000Z';
872
+ const episodicDate = '2024-03-03T00:00:00.000Z';
873
+
874
+ const { identityId, userMemoryId } = await userMemoryModel.addIdentityEntry({
875
+ base: {
876
+ lastAccessedAt: baseDate,
877
+ summary: 'Normalize base',
878
+ },
879
+ identity: {
880
+ description: 'Normalize identity',
881
+ episodicDate,
882
+ relationship: ' FRIEND ',
883
+ type: ' PERSONAL ',
884
+ },
885
+ });
886
+
887
+ const baseMemory = await serverDB.query.userMemories.findFirst({
888
+ where: eq(userMemories.id, userMemoryId),
889
+ });
890
+ const identityMemory = await serverDB.query.userMemoriesIdentities.findFirst({
891
+ where: eq(userMemoriesIdentities.id, identityId),
892
+ });
893
+
894
+ expect(baseMemory?.lastAccessedAt.toISOString()).toBe(new Date(baseDate).toISOString());
895
+ expect(identityMemory?.relationship).toBe('friend');
896
+ expect(identityMemory?.type).toBe('personal');
897
+ expect(identityMemory?.episodicDate?.toISOString()).toBe(
898
+ new Date(episodicDate).toISOString(),
899
+ );
900
+ });
901
+
902
+ it('falls back to defaults when invalid identity data is provided', async () => {
903
+ const frozenNow = new Date('2024-05-05T00:00:00.000Z');
904
+ vi.useFakeTimers();
905
+ vi.setSystemTime(frozenNow);
906
+
907
+ try {
908
+ const { identityId, userMemoryId } = await userMemoryModel.addIdentityEntry({
909
+ base: {
910
+ lastAccessedAt: 'not-a-date',
911
+ status: undefined,
912
+ },
913
+ identity: {
914
+ description: 'Invalid identity',
915
+ episodicDate: 'invalid',
916
+ relationship: 'bestie',
917
+ type: 'unknown',
918
+ },
919
+ });
920
+
921
+ const baseMemory = await serverDB.query.userMemories.findFirst({
922
+ where: eq(userMemories.id, userMemoryId),
923
+ });
924
+ const identityMemory = await serverDB.query.userMemoriesIdentities.findFirst({
925
+ where: eq(userMemoriesIdentities.id, identityId),
926
+ });
927
+
928
+ expect(baseMemory?.status).toBe('active');
929
+ expect(baseMemory?.lastAccessedAt.toISOString()).toBe(frozenNow.toISOString());
930
+ expect(identityMemory?.relationship).toBeNull();
931
+ expect(identityMemory?.type).toBeNull();
932
+ expect(identityMemory?.episodicDate).toBeNull();
933
+ } finally {
934
+ vi.useRealTimers();
935
+ }
936
+ });
937
+
938
+ it('updates identity entry fields with merge strategy', async () => {
939
+ const { identityId, userMemoryId } = await userMemoryModel.addIdentityEntry({
940
+ base: {
941
+ memoryCategory: 'profile',
942
+ summary: 'Original summary',
943
+ title: 'Original title',
944
+ },
945
+ identity: {
946
+ description: 'Original identity description',
947
+ relationship: 'friend',
948
+ role: 'developer',
949
+ type: 'personal',
950
+ },
951
+ });
952
+
953
+ const updated = await userMemoryModel.updateIdentityEntry({
954
+ identityId,
955
+ mergeStrategy: MergeStrategyEnum.Merge,
956
+ base: {
957
+ summary: 'Updated summary',
958
+ },
959
+ identity: {
960
+ description: 'Updated identity description',
961
+ relationship: 'mentor',
962
+ },
963
+ });
964
+
965
+ expect(updated).toBe(true);
966
+
967
+ const baseMemory = await serverDB.query.userMemories.findFirst({
968
+ where: eq(userMemories.id, userMemoryId),
969
+ });
970
+ const identityMemory = await serverDB.query.userMemoriesIdentities.findFirst({
971
+ where: eq(userMemoriesIdentities.id, identityId),
972
+ });
973
+
974
+ expect(baseMemory?.summary).toBe('Updated summary');
975
+ expect(baseMemory?.title).toBe('Original title');
976
+ expect(identityMemory?.description).toBe('Updated identity description');
977
+ expect(identityMemory?.relationship).toBe('mentor');
978
+ expect(identityMemory?.role).toBe('developer');
979
+ });
980
+
981
+ it('normalizes identity values and coerces dates when updating entries', async () => {
982
+ const { identityId, userMemoryId } = await userMemoryModel.addIdentityEntry({
983
+ base: {
984
+ summary: 'base summary',
985
+ },
986
+ identity: {
987
+ description: 'base identity',
988
+ },
989
+ });
990
+
991
+ await userMemoryModel.updateIdentityEntry({
992
+ identityId,
993
+ base: {
994
+ lastAccessedAt: '2024-06-06T00:00:00.000Z',
995
+ },
996
+ identity: {
997
+ episodicDate: '2024-07-07T00:00:00.000Z',
998
+ relationship: ' COLLEAGUE ',
999
+ type: ' PROFESSIONAL ',
1000
+ },
1001
+ });
1002
+
1003
+ const baseMemory = await serverDB.query.userMemories.findFirst({
1004
+ where: eq(userMemories.id, userMemoryId),
1005
+ });
1006
+ const identityMemory = await serverDB.query.userMemoriesIdentities.findFirst({
1007
+ where: eq(userMemoriesIdentities.id, identityId),
1008
+ });
1009
+
1010
+ expect(baseMemory?.lastAccessedAt.toISOString()).toBe('2024-06-06T00:00:00.000Z');
1011
+ expect(identityMemory?.relationship).toBe('colleague');
1012
+ expect(identityMemory?.type).toBe('professional');
1013
+ expect(identityMemory?.episodicDate?.toISOString()).toBe('2024-07-07T00:00:00.000Z');
1014
+ });
1015
+
1016
+ it('replaces identity entry fields and clears unspecified values when mergeStrategy is replace', async () => {
1017
+ const { identityId } = await userMemoryModel.addIdentityEntry({
1018
+ base: {
1019
+ summary: 'Summary to replace',
1020
+ title: 'Title to replace',
1021
+ },
1022
+ identity: {
1023
+ description: 'Description to replace',
1024
+ relationship: 'mentor',
1025
+ role: 'manager',
1026
+ type: 'professional',
1027
+ },
1028
+ });
1029
+
1030
+ const replaced = await userMemoryModel.updateIdentityEntry({
1031
+ identityId,
1032
+ mergeStrategy: MergeStrategyEnum.Replace,
1033
+ identity: {
1034
+ description: 'Fresh description',
1035
+ type: 'personal',
1036
+ },
1037
+ });
1038
+
1039
+ expect(replaced).toBe(true);
1040
+
1041
+ const identityMemory = await serverDB.query.userMemoriesIdentities.findFirst({
1042
+ where: eq(userMemoriesIdentities.id, identityId),
1043
+ });
1044
+
1045
+ expect(identityMemory?.description).toBe('Fresh description');
1046
+ expect(identityMemory?.type).toBe('personal');
1047
+ expect(identityMemory?.relationship).toBeNull();
1048
+ expect(identityMemory?.role).toBeNull();
1049
+ });
1050
+
1051
+ it('keeps existing identity fields when merge payload is empty', async () => {
1052
+ const { identityId } = await userMemoryModel.addIdentityEntry({
1053
+ base: { summary: 'keep base' },
1054
+ identity: {
1055
+ description: 'Keep description',
1056
+ metadata: { keep: true },
1057
+ relationship: 'colleague',
1058
+ role: 'engineer',
1059
+ tags: ['keep'],
1060
+ type: 'professional',
1061
+ },
1062
+ });
1063
+
1064
+ await userMemoryModel.updateIdentityEntry({
1065
+ identityId,
1066
+ identity: {},
1067
+ mergeStrategy: MergeStrategyEnum.Merge,
1068
+ });
1069
+
1070
+ const identityMemory = await serverDB.query.userMemoriesIdentities.findFirst({
1071
+ where: eq(userMemoriesIdentities.id, identityId),
1072
+ });
1073
+
1074
+ expect(identityMemory?.description).toBe('Keep description');
1075
+ expect(identityMemory?.metadata).toEqual({ keep: true });
1076
+ expect(identityMemory?.relationship).toBe('colleague');
1077
+ expect(identityMemory?.role).toBe('engineer');
1078
+ expect(identityMemory?.tags).toEqual(['keep']);
1079
+ expect(identityMemory?.type).toBe('professional');
1080
+ });
1081
+
1082
+ it('clears all identity fields when replacing with an empty payload', async () => {
1083
+ const { identityId } = await userMemoryModel.addIdentityEntry({
1084
+ base: { summary: 'keep base' },
1085
+ identity: {
1086
+ description: 'Will be cleared',
1087
+ metadata: { existing: true },
1088
+ relationship: 'mentor',
1089
+ role: 'lead',
1090
+ tags: ['tagged'],
1091
+ type: 'personal',
1092
+ },
1093
+ });
1094
+
1095
+ await userMemoryModel.updateIdentityEntry({
1096
+ identityId,
1097
+ identity: {},
1098
+ mergeStrategy: MergeStrategyEnum.Replace,
1099
+ });
1100
+
1101
+ const identityMemory = await serverDB.query.userMemoriesIdentities.findFirst({
1102
+ where: eq(userMemoriesIdentities.id, identityId),
1103
+ });
1104
+
1105
+ expect(identityMemory?.description).toBeNull();
1106
+ expect(identityMemory?.metadata).toBeNull();
1107
+ expect(identityMemory?.relationship).toBeNull();
1108
+ expect(identityMemory?.role).toBeNull();
1109
+ expect(identityMemory?.tags).toBeNull();
1110
+ expect(identityMemory?.type).toBeNull();
1111
+ });
1112
+
1113
+ it('removes identity entry and associated base memory', async () => {
1114
+ const { identityId, userMemoryId } = await userMemoryModel.addIdentityEntry({
1115
+ base: {
1116
+ summary: 'Summary to delete',
1117
+ },
1118
+ identity: {
1119
+ description: 'Identity to delete',
1120
+ },
1121
+ });
1122
+
1123
+ const removed = await userMemoryModel.removeIdentityEntry(identityId);
1124
+
1125
+ const baseMemory = await serverDB.query.userMemories.findFirst({
1126
+ where: eq(userMemories.id, userMemoryId),
1127
+ });
1128
+ const identityMemory = await serverDB.query.userMemoriesIdentities.findFirst({
1129
+ where: eq(userMemoriesIdentities.id, identityId),
1130
+ });
1131
+
1132
+ expect(removed).toBe(true);
1133
+ expect(baseMemory).toBeUndefined();
1134
+ expect(identityMemory).toBeUndefined();
1135
+ });
1136
+ });
1137
+
1138
+ describe('getAllIdentities', () => {
1139
+ it('returns all identities for current user ordered by newest first', async () => {
1140
+ const first = await userMemoryModel.addIdentityEntry({
1141
+ base: { summary: 'first' },
1142
+ identity: { description: 'first identity', type: 'personal' },
1143
+ });
1144
+
1145
+ const second = await userMemoryModel.addIdentityEntry({
1146
+ base: { summary: 'second' },
1147
+ identity: { description: 'second identity', type: 'professional' },
1148
+ });
1149
+
1150
+ const third = await userMemoryModel.addIdentityEntry({
1151
+ base: { summary: 'third' },
1152
+ identity: { description: 'third identity', type: 'demographic' },
1153
+ });
1154
+
1155
+ const anotherUserModel = new UserMemoryModel(serverDB, userId2);
1156
+ await anotherUserModel.addIdentityEntry({
1157
+ base: { summary: 'other-user' },
1158
+ identity: { description: 'other identity', type: 'personal' },
1159
+ });
1160
+
1161
+ await serverDB
1162
+ .update(userMemoriesIdentities)
1163
+ .set({ createdAt: new Date('2024-01-01T00:00:00.000Z') })
1164
+ .where(eq(userMemoriesIdentities.id, first.identityId));
1165
+ await serverDB
1166
+ .update(userMemoriesIdentities)
1167
+ .set({ createdAt: new Date('2024-02-01T00:00:00.000Z') })
1168
+ .where(eq(userMemoriesIdentities.id, second.identityId));
1169
+ await serverDB
1170
+ .update(userMemoriesIdentities)
1171
+ .set({ createdAt: new Date('2024-03-01T00:00:00.000Z') })
1172
+ .where(eq(userMemoriesIdentities.id, third.identityId));
1173
+
1174
+ const identities = await userMemoryModel.getAllIdentities();
1175
+
1176
+ expect(identities).toHaveLength(3);
1177
+ expect(identities[0]?.id).toBe(third.identityId);
1178
+ expect(identities[1]?.id).toBe(second.identityId);
1179
+ expect(identities[2]?.id).toBe(first.identityId);
1180
+ identities.forEach((identity) => {
1181
+ expect(identity.userId).toBe(userId);
1182
+ });
1183
+ });
1184
+ });
1185
+
1186
+ describe('getIdentitiesByType', () => {
1187
+ it('returns the newest identities of the requested type for the current user', async () => {
1188
+ const resultOne = await userMemoryModel.addIdentityEntry({
1189
+ base: {
1190
+ summary: 'personal-one',
1191
+ },
1192
+ identity: {
1193
+ description: 'first personal',
1194
+ type: 'personal',
1195
+ },
1196
+ });
1197
+
1198
+ const resultTwo = await userMemoryModel.addIdentityEntry({
1199
+ base: {
1200
+ summary: 'personal-two',
1201
+ },
1202
+ identity: {
1203
+ description: 'second personal',
1204
+ type: 'personal',
1205
+ },
1206
+ });
1207
+
1208
+ await userMemoryModel.addIdentityEntry({
1209
+ base: {
1210
+ summary: 'preference-not-personal',
1211
+ },
1212
+ identity: {
1213
+ description: 'professional identity',
1214
+ type: 'professional',
1215
+ },
1216
+ });
1217
+
1218
+ const anotherUserModel = new UserMemoryModel(serverDB, userId2);
1219
+ await anotherUserModel.addIdentityEntry({
1220
+ base: {
1221
+ summary: 'other user personal',
1222
+ },
1223
+ identity: {
1224
+ description: 'should not show',
1225
+ type: 'personal',
1226
+ },
1227
+ });
1228
+
1229
+ // Ensure deterministic ordering by forcing createdAt values
1230
+ await serverDB
1231
+ .update(userMemoriesIdentities)
1232
+ .set({ createdAt: new Date('2024-01-01T00:00:00.000Z') })
1233
+ .where(eq(userMemoriesIdentities.id, resultOne.identityId));
1234
+ await serverDB
1235
+ .update(userMemoriesIdentities)
1236
+ .set({ createdAt: new Date('2024-02-01T00:00:00.000Z') })
1237
+ .where(eq(userMemoriesIdentities.id, resultTwo.identityId));
1238
+
1239
+ const identities = await userMemoryModel.getIdentitiesByType('personal');
1240
+
1241
+ expect(identities).toHaveLength(2);
1242
+ expect(identities[0]?.id).toBe(resultTwo.identityId);
1243
+ expect(identities[1]?.id).toBe(resultOne.identityId);
1244
+ identities.forEach((identity) => {
1245
+ expect(identity.type).toBe('personal');
1246
+ expect(identity.userId).toBe(userId);
1247
+ });
1248
+ });
1249
+ });
1250
+
1251
+ describe('access metrics', () => {
1252
+ it('should update accessedAt for each layer table', async () => {
1253
+ const experienceParams = generateRandomCreateUserMemoryExperienceParams();
1254
+ const experienceMemory = await userMemoryModel.create(experienceParams);
1255
+ await serverDB.insert(userMemoriesExperiences).values({
1256
+ ...experienceParams.experience,
1257
+ userId,
1258
+ userMemoryId: experienceMemory.id,
1259
+ });
1260
+
1261
+ const preferenceParams = generateRandomCreateUserMemoryPreferenceParams();
1262
+ const preferenceMemory = await userMemoryModel.create(preferenceParams);
1263
+ await serverDB.insert(userMemoriesPreferences).values({
1264
+ ...preferenceParams.preference,
1265
+ userId,
1266
+ userMemoryId: preferenceMemory.id,
1267
+ });
1268
+
1269
+ const contextParams = generateRandomCreateUserMemoryContextParams();
1270
+ await serverDB.insert(userMemoriesContexts).values({
1271
+ ...contextParams.context,
1272
+ userId,
1273
+ userMemoryIds: [experienceMemory.id, preferenceMemory.id],
1274
+ });
1275
+
1276
+ const beforeContexts = await serverDB.query.userMemoriesContexts.findMany({
1277
+ where: eq(userMemoriesContexts.userId, userId),
1278
+ });
1279
+
1280
+ const beforeExperience = await serverDB.query.userMemoriesExperiences.findFirst({
1281
+ where: eq(userMemoriesExperiences.userMemoryId, experienceMemory.id),
1282
+ });
1283
+ const beforePreference = await serverDB.query.userMemoriesPreferences.findFirst({
1284
+ where: eq(userMemoriesPreferences.userMemoryId, preferenceMemory.id),
1285
+ });
1286
+
1287
+ const beforeBaseMemories = await serverDB.query.userMemories.findMany({
1288
+ where: eq(userMemories.userId, userId),
1289
+ });
1290
+
1291
+ await userMemoryModel.search({ limit: 10 });
1292
+
1293
+ const afterExperience = await serverDB.query.userMemoriesExperiences.findFirst({
1294
+ where: eq(userMemoriesExperiences.userMemoryId, experienceMemory.id),
1295
+ });
1296
+ const afterPreference = await serverDB.query.userMemoriesPreferences.findFirst({
1297
+ where: eq(userMemoriesPreferences.userMemoryId, preferenceMemory.id),
1298
+ });
1299
+ const afterContexts = await serverDB.query.userMemoriesContexts.findMany({
1300
+ where: eq(userMemoriesContexts.userId, userId),
1301
+ });
1302
+ const afterBaseMemoryMap = new Map(
1303
+ (
1304
+ await serverDB.query.userMemories.findMany({
1305
+ where: eq(userMemories.userId, userId),
1306
+ })
1307
+ ).map((memory) => [memory.id, memory]),
1308
+ );
1309
+
1310
+ expect(beforeExperience?.accessedAt?.getTime()).toBeDefined();
1311
+ expect(afterExperience?.accessedAt?.getTime()).toBeDefined();
1312
+ expect(afterExperience!.accessedAt.getTime()).toBeGreaterThanOrEqual(
1313
+ beforeExperience!.accessedAt.getTime(),
1314
+ );
1315
+
1316
+ expect(beforePreference?.accessedAt?.getTime()).toBeDefined();
1317
+ expect(afterPreference?.accessedAt?.getTime()).toBeDefined();
1318
+ expect(afterPreference!.accessedAt.getTime()).toBeGreaterThanOrEqual(
1319
+ beforePreference!.accessedAt.getTime(),
1320
+ );
1321
+
1322
+ beforeContexts.forEach((beforeContext) => {
1323
+ const afterContext = afterContexts.find((ctx) => ctx.id === beforeContext.id);
1324
+ expect(afterContext).toBeDefined();
1325
+ expect(afterContext!.accessedAt.getTime()).toBeGreaterThanOrEqual(
1326
+ beforeContext.accessedAt.getTime(),
1327
+ );
1328
+ });
1329
+
1330
+ const updatedIds = new Set([experienceMemory.id, preferenceMemory.id]);
1331
+
1332
+ for (const before of beforeBaseMemories) {
1333
+ const after = afterBaseMemoryMap.get(before.id);
1334
+ expect(after).toBeDefined();
1335
+ if (updatedIds.has(before.id)) {
1336
+ expect(after!.accessedCount).toBeDefined();
1337
+ expect(after!.accessedCount).toBe((before.accessedCount ?? 0) + 1);
1338
+ expect(after!.accessedAt.getTime()).toBeGreaterThanOrEqual(before.accessedAt.getTime());
1339
+ expect(after!.lastAccessedAt.getTime()).toBeGreaterThanOrEqual(
1340
+ before.lastAccessedAt.getTime(),
1341
+ );
1342
+ } else {
1343
+ expect(after!.accessedCount).toBe(before.accessedCount);
1344
+ }
1345
+ }
1346
+ });
1347
+
1348
+ it('updates accessedAt on identity layer records when base memory is identity', async () => {
1349
+ const { identityId, userMemoryId } = await userMemoryModel.addIdentityEntry({
1350
+ base: {
1351
+ memoryLayer: 'identity',
1352
+ summary: 'identity base',
1353
+ },
1354
+ identity: {
1355
+ description: 'identity description',
1356
+ type: 'personal',
1357
+ },
1358
+ });
1359
+
1360
+ const past = new Date('2023-01-01T00:00:00.000Z');
1361
+ await serverDB
1362
+ .update(userMemoriesIdentities)
1363
+ .set({ accessedAt: past })
1364
+ .where(eq(userMemoriesIdentities.id, identityId));
1365
+
1366
+ const before = await serverDB.query.userMemoriesIdentities.findFirst({
1367
+ where: eq(userMemoriesIdentities.id, identityId),
1368
+ });
1369
+ expect(before).toBeDefined();
1370
+ expect(before?.accessedAt.getTime()).toBe(past.getTime());
1371
+
1372
+ await userMemoryModel.findById(userMemoryId);
1373
+
1374
+ const after = await serverDB.query.userMemoriesIdentities.findFirst({
1375
+ where: eq(userMemoriesIdentities.id, identityId),
1376
+ });
1377
+
1378
+ expect(after).toBeDefined();
1379
+ expect(after!.accessedAt.getTime()).toBeGreaterThan(past.getTime());
1380
+ });
1381
+ });
1382
+ });