@lobehub/lobehub 2.0.0-next.342 → 2.0.0-next.343

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 (79) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/changelog/v1.json +12 -0
  3. package/package.json +1 -1
  4. package/packages/database/src/models/__tests__/userMemories.test.ts +62 -5
  5. package/packages/database/src/models/agentCronJob.ts +9 -9
  6. package/packages/database/src/models/userMemory/__tests__/identity.test.ts +5 -5
  7. package/packages/database/src/models/userMemory/experience.ts +91 -1
  8. package/packages/database/src/models/userMemory/identity.ts +93 -2
  9. package/packages/database/src/models/userMemory/model.ts +27 -8
  10. package/packages/types/src/userMemory/experience.ts +25 -0
  11. package/packages/types/src/userMemory/identity.ts +27 -0
  12. package/packages/types/src/userMemory/index.ts +1 -0
  13. package/packages/types/src/userMemory/shared.ts +30 -0
  14. package/src/app/[variants]/(main)/group/profile/features/Header/index.tsx +3 -4
  15. package/src/app/[variants]/(main)/home/features/InputArea/SkillInstallBanner.tsx +7 -8
  16. package/src/app/[variants]/(main)/memory/(home)/features/Persona/PersonaDetail.tsx +58 -0
  17. package/src/app/[variants]/(main)/memory/(home)/features/Persona/PersonaHeader.tsx +22 -0
  18. package/src/app/[variants]/(main)/memory/(home)/features/Persona/PersonaSummary.tsx +43 -0
  19. package/src/app/[variants]/(main)/memory/(home)/features/Persona/index.tsx +53 -0
  20. package/src/app/[variants]/(main)/memory/(home)/features/RoleTagCloud/index.tsx +2 -2
  21. package/src/app/[variants]/(main)/memory/(home)/index.tsx +15 -3
  22. package/src/app/[variants]/(main)/memory/experiences/features/List/GridView/ExperienceCard.tsx +3 -3
  23. package/src/app/[variants]/(main)/memory/experiences/features/List/GridView/index.tsx +3 -3
  24. package/src/app/[variants]/(main)/memory/experiences/features/List/TimelineView/ExperienceCard.tsx +3 -3
  25. package/src/app/[variants]/(main)/memory/experiences/features/List/TimelineView/index.tsx +3 -3
  26. package/src/app/[variants]/(main)/memory/features/SourceLink.tsx +2 -11
  27. package/src/app/[variants]/(main)/memory/features/TimeLineView/TimeLineCard.tsx +2 -9
  28. package/src/app/[variants]/(main)/memory/identities/features/IdentityRightPanel.tsx +1 -1
  29. package/src/app/[variants]/(main)/memory/identities/features/List/GridView/IdentityCard.tsx +5 -4
  30. package/src/app/[variants]/(main)/memory/identities/features/List/GridView/index.tsx +3 -3
  31. package/src/app/[variants]/(main)/memory/identities/features/List/TimelineView/IdentityCard.tsx +6 -6
  32. package/src/app/[variants]/(main)/memory/identities/features/List/TimelineView/index.tsx +6 -4
  33. package/src/app/[variants]/(main)/settings/profile/index.tsx +8 -8
  34. package/src/app/[variants]/(main)/settings/provider/features/ProviderConfig/index.tsx +0 -1
  35. package/src/app/[variants]/(main)/settings/skill/features/Actions.tsx +0 -1
  36. package/src/app/[variants]/(main)/settings/skill/features/KlavisSkillItem.tsx +9 -10
  37. package/src/app/[variants]/(main)/settings/skill/features/LobehubSkillItem.tsx +9 -10
  38. package/src/app/[variants]/(main)/settings/skill/features/McpSkillItem.tsx +4 -5
  39. package/src/app/[variants]/(main)/settings/skill/features/SkillList.tsx +4 -5
  40. package/src/app/[variants]/share/t/[id]/SharedMessageList.tsx +1 -4
  41. package/src/app/[variants]/share/t/[id]/_layout/index.tsx +47 -121
  42. package/src/app/[variants]/share/t/[id]/_layout/style.ts +59 -0
  43. package/src/app/[variants]/share/t/[id]/features/Portal/index.tsx +4 -5
  44. package/src/app/[variants]/share/t/[id]/index.tsx +30 -37
  45. package/src/components/404/index.tsx +15 -9
  46. package/src/components/DragUpload/index.tsx +15 -16
  47. package/src/features/EditorCanvas/DocumentIdMode.tsx +1 -2
  48. package/src/features/IntegrationDetailModal/index.tsx +11 -12
  49. package/src/features/ResourceManager/index.tsx +13 -6
  50. package/src/features/ShareModal/ShareImage/Preview.tsx +19 -28
  51. package/src/features/ShareModal/ShareImage/style.ts +4 -2
  52. package/src/features/ShareModal/index.tsx +5 -1
  53. package/src/features/ShareModal/style.ts +1 -0
  54. package/src/features/ShareModal/useContainerStyles.ts +1 -1
  55. package/src/features/SharePopover/index.tsx +16 -9
  56. package/src/features/SharePopover/style.ts +2 -2
  57. package/src/features/SkillStore/CommunityList/Item.tsx +2 -2
  58. package/src/features/SkillStore/LobeHubList/Item.tsx +2 -2
  59. package/src/features/SkillStore/LobeHubList/index.tsx +2 -3
  60. package/src/features/SkillStore/style.ts +4 -4
  61. package/src/layout/GlobalProvider/ServerVersionOutdatedAlert.tsx +28 -20
  62. package/src/server/routers/lambda/userMemories.ts +61 -5
  63. package/src/server/routers/lambda/userMemory.ts +5 -1
  64. package/src/services/chat/index.ts +2 -2
  65. package/src/services/userMemory/index.ts +25 -1
  66. package/src/store/chat/slices/aiChat/actions/streamingExecutor.ts +0 -1
  67. package/src/store/userMemory/initialState.ts +22 -52
  68. package/src/store/userMemory/slices/context/action.ts +1 -1
  69. package/src/store/userMemory/slices/context/index.ts +1 -0
  70. package/src/store/userMemory/slices/context/initialState.ts +22 -0
  71. package/src/store/userMemory/slices/experience/action.ts +10 -22
  72. package/src/store/userMemory/slices/experience/index.ts +1 -0
  73. package/src/store/userMemory/slices/experience/initialState.ts +22 -0
  74. package/src/store/userMemory/slices/home/action.ts +17 -0
  75. package/src/store/userMemory/slices/identity/action.ts +36 -24
  76. package/src/store/userMemory/slices/identity/initialState.ts +7 -4
  77. package/src/store/userMemory/slices/preference/action.ts +1 -1
  78. package/src/store/userMemory/slices/preference/index.ts +1 -0
  79. package/src/store/userMemory/slices/preference/initialState.ts +22 -0
package/CHANGELOG.md CHANGED
@@ -2,6 +2,47 @@
2
2
 
3
3
  # Changelog
4
4
 
5
+ ## [Version 2.0.0-next.343](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.342...v2.0.0-next.343)
6
+
7
+ <sup>Released on **2026-01-23**</sup>
8
+
9
+ #### ♻ Code Refactoring
10
+
11
+ - **misc**: Improve memory data with experience and identity.
12
+
13
+ #### 🐛 Bug Fixes
14
+
15
+ - **misc**: Fix scope issue.
16
+
17
+ #### 💄 Styles
18
+
19
+ - **misc**: Update share style.
20
+
21
+ <br/>
22
+
23
+ <details>
24
+ <summary><kbd>Improvements and Fixes</kbd></summary>
25
+
26
+ #### Code refactoring
27
+
28
+ - **misc**: Improve memory data with experience and identity, closes [#11717](https://github.com/lobehub/lobe-chat/issues/11717) ([bdb3eb4](https://github.com/lobehub/lobe-chat/commit/bdb3eb4))
29
+
30
+ #### What's fixed
31
+
32
+ - **misc**: Fix scope issue, closes [#11719](https://github.com/lobehub/lobe-chat/issues/11719) ([17adde8](https://github.com/lobehub/lobe-chat/commit/17adde8))
33
+
34
+ #### Styles
35
+
36
+ - **misc**: Update share style, closes [#11716](https://github.com/lobehub/lobe-chat/issues/11716) ([3c70dfa](https://github.com/lobehub/lobe-chat/commit/3c70dfa))
37
+
38
+ </details>
39
+
40
+ <div align="right">
41
+
42
+ [![](https://img.shields.io/badge/-BACK_TO_TOP-151515?style=flat-square)](#readme-top)
43
+
44
+ </div>
45
+
5
46
  ## [Version 2.0.0-next.342](https://github.com/lobehub/lobe-chat/compare/v2.0.0-next.341...v2.0.0-next.342)
6
47
 
7
48
  <sup>Released on **2026-01-22**</sup>
package/changelog/v1.json CHANGED
@@ -1,4 +1,16 @@
1
1
  [
2
+ {
3
+ "children": {
4
+ "improvements": [
5
+ "Update share style."
6
+ ],
7
+ "fixes": [
8
+ "Fix scope issue."
9
+ ]
10
+ },
11
+ "date": "2026-01-23",
12
+ "version": "2.0.0-next.343"
13
+ },
2
14
  {
3
15
  "children": {},
4
16
  "date": "2026-01-22",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lobehub/lobehub",
3
- "version": "2.0.0-next.342",
3
+ "version": "2.0.0-next.343",
4
4
  "description": "LobeHub - an open-source,comprehensive AI Agent framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.",
5
5
  "keywords": [
6
6
  "framework",
@@ -6,6 +6,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
6
6
 
7
7
  import { idGenerator } from '@/database/utils/idGenerator';
8
8
 
9
+ import { getTestDB } from '../../core/getTestDB';
9
10
  import {
10
11
  topics,
11
12
  userMemories,
@@ -24,7 +25,6 @@ import {
24
25
  CreateUserMemoryPreferenceParams,
25
26
  UserMemoryModel,
26
27
  } from '../userMemory';
27
- import { getTestDB } from '../../core/getTestDB';
28
28
 
29
29
  const serverDB: LobeChatDatabase = await getTestDB();
30
30
 
@@ -753,20 +753,27 @@ describe('UserMemoryModel', () => {
753
753
 
754
754
  await userMemoryModel.addIdentityEntry({
755
755
  base: { lastAccessedAt: now, tags: [] },
756
- identity: { role: 'engineer', tags: ['alpha', 'beta'] },
756
+ identity: { relationship: 'self', role: 'engineer', tags: ['alpha', 'beta'] },
757
757
  });
758
758
  await userMemoryModel.addIdentityEntry({
759
759
  base: { lastAccessedAt: now, tags: [] },
760
- identity: { role: 'engineer', tags: ['alpha'] },
760
+ identity: { relationship: 'self', role: 'engineer', tags: ['alpha'] },
761
761
  });
762
762
  await userMemoryModel.addIdentityEntry({
763
763
  base: { lastAccessedAt: now, tags: [] },
764
- identity: { role: 'manager', tags: [] },
764
+ identity: { relationship: 'self', role: 'manager', tags: [] },
765
765
  });
766
766
 
767
+ // This should not be counted (different user)
767
768
  await anotherUserModel.addIdentityEntry({
768
769
  base: { lastAccessedAt: now, tags: [] },
769
- identity: { role: 'engineer', tags: ['alpha'] },
770
+ identity: { relationship: 'self', role: 'engineer', tags: ['alpha'] },
771
+ });
772
+
773
+ // This should not be counted (relationship is not 'self')
774
+ await userMemoryModel.addIdentityEntry({
775
+ base: { lastAccessedAt: now, tags: [] },
776
+ identity: { relationship: 'friend', role: 'designer', tags: ['gamma'] },
770
777
  });
771
778
 
772
779
  const result = await userMemoryModel.queryIdentityRoles({ size: 5 });
@@ -1062,6 +1069,56 @@ describe('UserMemoryModel', () => {
1062
1069
  expect(identityItem.identity.userMemoryId).toBe(identityMemoryId);
1063
1070
  expect(identityItem.identity.type).toBe(identity?.type);
1064
1071
  });
1072
+
1073
+ it('should order identity memories by capturedAt desc and include capturedAt and title in response', async () => {
1074
+ const olderCapturedAt = new Date('2024-01-01T10:00:00Z');
1075
+ const newerCapturedAt = new Date('2024-01-15T10:00:00Z');
1076
+
1077
+ await userMemoryModel.addIdentityEntry({
1078
+ base: { summary: 'older identity', title: 'Older Title' },
1079
+ identity: {
1080
+ capturedAt: olderCapturedAt,
1081
+ description: 'Older identity description',
1082
+ relationship: 'friend',
1083
+ type: 'personal',
1084
+ },
1085
+ });
1086
+
1087
+ await userMemoryModel.addIdentityEntry({
1088
+ base: { summary: 'newer identity', title: 'Newer Title' },
1089
+ identity: {
1090
+ capturedAt: newerCapturedAt,
1091
+ description: 'Newer identity description',
1092
+ relationship: 'self',
1093
+ type: 'personal',
1094
+ },
1095
+ });
1096
+
1097
+ const result = await userMemoryModel.queryMemories({
1098
+ layer: LayersEnum.Identity,
1099
+ });
1100
+
1101
+ expect(result.total).toBe(2);
1102
+ expect(result.items).toHaveLength(2);
1103
+
1104
+ const firstItem = result.items[0] as any;
1105
+ const secondItem = result.items[1] as any;
1106
+
1107
+ // Verify order by capturedAt desc
1108
+ expect(firstItem.identity.capturedAt).toEqual(newerCapturedAt);
1109
+ expect(secondItem.identity.capturedAt).toEqual(olderCapturedAt);
1110
+
1111
+ expect(firstItem.identity.description).toBe('Newer identity description');
1112
+ expect(secondItem.identity.description).toBe('Older identity description');
1113
+
1114
+ // Verify title comes from memory schema
1115
+ expect(firstItem.identity.title).toBe('Newer Title');
1116
+ expect(secondItem.identity.title).toBe('Older Title');
1117
+
1118
+ // Verify relationship is included
1119
+ expect(firstItem.identity.relationship).toBe('self');
1120
+ expect(secondItem.identity.relationship).toBe('friend');
1121
+ });
1065
1122
  });
1066
1123
 
1067
1124
  describe('findById', () => {
@@ -1,4 +1,4 @@
1
- import { and, desc, eq, gt, isNull, or, sql } from 'drizzle-orm';
1
+ import { and, desc, eq, gt, inArray, isNull, or, sql } from 'drizzle-orm';
2
2
 
3
3
  import {
4
4
  type AgentCronJob,
@@ -25,8 +25,8 @@ export class AgentCronJobModel {
25
25
  .values({
26
26
  ...data,
27
27
  // Initialize remaining executions to match max executions
28
- remainingExecutions: data.maxExecutions,
29
-
28
+ remainingExecutions: data.maxExecutions,
29
+
30
30
  userId: this.userId,
31
31
  } as NewAgentCronJob)
32
32
  .returning();
@@ -149,11 +149,11 @@ remainingExecutions: data.maxExecutions,
149
149
  .set({
150
150
  enabled: true,
151
151
  // Re-enable job when resetting
152
- lastExecutedAt: null,
153
-
154
- maxExecutions: newMaxExecutions,
155
-
156
- remainingExecutions: newMaxExecutions,
152
+ lastExecutedAt: null,
153
+
154
+ maxExecutions: newMaxExecutions,
155
+
156
+ remainingExecutions: newMaxExecutions,
157
157
  totalExecutions: 0,
158
158
  updatedAt: new Date(),
159
159
  })
@@ -227,7 +227,7 @@ remainingExecutions: newMaxExecutions,
227
227
  enabled,
228
228
  updatedAt: new Date(),
229
229
  })
230
- .where(and(sql`${agentCronJobs.id} = ANY(${ids})`, eq(agentCronJobs.userId, this.userId)))
230
+ .where(and(inArray(agentCronJobs.id, ids), eq(agentCronJobs.userId, this.userId)))
231
231
  .returning();
232
232
 
233
233
  return result.length;
@@ -2,6 +2,7 @@
2
2
  import { RelationshipEnum } from '@lobechat/types';
3
3
  import { beforeEach, describe, expect, it } from 'vitest';
4
4
 
5
+ import { getTestDB } from '../../../core/getTestDB';
5
6
  import {
6
7
  NewUserMemoryIdentity,
7
8
  userMemories,
@@ -9,7 +10,6 @@ import {
9
10
  users,
10
11
  } from '../../../schemas';
11
12
  import { LobeChatDatabase } from '../../../type';
12
- import { getTestDB } from '../../../core/getTestDB';
13
13
  import { UserMemoryIdentityModel } from '../identity';
14
14
 
15
15
  const userId = 'identity-test-user';
@@ -68,21 +68,21 @@ describe('UserMemoryIdentityModel', () => {
68
68
  userId,
69
69
  type: 'personal',
70
70
  description: 'Identity 1',
71
- createdAt: new Date('2024-01-01T10:00:00Z'),
71
+ capturedAt: new Date('2024-01-01T10:00:00Z'),
72
72
  },
73
73
  {
74
74
  id: 'identity-2',
75
75
  userId,
76
76
  type: 'professional',
77
77
  description: 'Identity 2',
78
- createdAt: new Date('2024-01-02T10:00:00Z'),
78
+ capturedAt: new Date('2024-01-02T10:00:00Z'),
79
79
  },
80
80
  {
81
81
  id: 'other-identity',
82
82
  userId: otherUserId,
83
83
  type: 'personal',
84
84
  description: 'Other Identity',
85
- createdAt: new Date('2024-01-03T10:00:00Z'),
85
+ capturedAt: new Date('2024-01-03T10:00:00Z'),
86
86
  },
87
87
  ]);
88
88
  });
@@ -94,7 +94,7 @@ describe('UserMemoryIdentityModel', () => {
94
94
  expect(result.every((i) => i.userId === userId)).toBe(true);
95
95
  });
96
96
 
97
- it('should order by createdAt desc', async () => {
97
+ it('should order by capturedAt desc', async () => {
98
98
  const result = await identityModel.query();
99
99
 
100
100
  expect(result[0].id).toBe('identity-2'); // Most recent first
@@ -1,4 +1,5 @@
1
- import { and, desc, eq } from 'drizzle-orm';
1
+ import type { ExperienceListParams, ExperienceListResult } from '@lobechat/types';
2
+ import { type SQL, and, asc, desc, eq, ilike, inArray, or, sql } from 'drizzle-orm';
2
3
 
3
4
  import {
4
5
  NewUserMemoryExperience,
@@ -64,6 +65,95 @@ export class UserMemoryExperienceModel {
64
65
  });
65
66
  };
66
67
 
68
+ /**
69
+ * Query experience list with pagination, search, and sorting
70
+ * Returns a flat structure optimized for frontend display
71
+ */
72
+ queryList = async (params: ExperienceListParams = {}): Promise<ExperienceListResult> => {
73
+ const { order = 'desc', page = 1, pageSize = 20, q, sort, tags, types } = params;
74
+
75
+ const normalizedPage = Math.max(1, page);
76
+ const normalizedPageSize = Math.min(Math.max(pageSize, 1), 100);
77
+ const offset = (normalizedPage - 1) * normalizedPageSize;
78
+ const normalizedQuery = typeof q === 'string' ? q.trim() : '';
79
+
80
+ // Build WHERE conditions
81
+ const conditions: Array<SQL | undefined> = [
82
+ eq(userMemoriesExperiences.userId, this.userId),
83
+ // Full-text search across title, situation, keyLearning, action
84
+ normalizedQuery
85
+ ? or(
86
+ ilike(userMemories.title, `%${normalizedQuery}%`),
87
+ ilike(userMemoriesExperiences.situation, `%${normalizedQuery}%`),
88
+ ilike(userMemoriesExperiences.keyLearning, `%${normalizedQuery}%`),
89
+ ilike(userMemoriesExperiences.action, `%${normalizedQuery}%`),
90
+ )
91
+ : undefined,
92
+ types && types.length > 0 ? inArray(userMemoriesExperiences.type, types) : undefined,
93
+ tags && tags.length > 0
94
+ ? or(...tags.map((tag) => sql<boolean>`${tag} = ANY(${userMemoriesExperiences.tags})`))
95
+ : undefined,
96
+ ];
97
+
98
+ const filters = conditions.filter((condition): condition is SQL => condition !== undefined);
99
+ const whereClause = filters.length > 0 ? and(...filters) : undefined;
100
+
101
+ // Build ORDER BY
102
+ const applyOrder = order === 'asc' ? asc : desc;
103
+ const sortColumn =
104
+ sort === 'scoreConfidence'
105
+ ? userMemoriesExperiences.scoreConfidence
106
+ : userMemoriesExperiences.capturedAt;
107
+
108
+ const orderByClauses = [
109
+ applyOrder(sortColumn),
110
+ applyOrder(userMemoriesExperiences.updatedAt),
111
+ applyOrder(userMemoriesExperiences.createdAt),
112
+ ];
113
+
114
+ // JOIN condition
115
+ const joinCondition = and(
116
+ eq(userMemories.id, userMemoriesExperiences.userMemoryId),
117
+ eq(userMemories.userId, this.userId),
118
+ );
119
+
120
+ // Execute queries in parallel
121
+ const [rows, totalResult] = await Promise.all([
122
+ this.db
123
+ .select({
124
+ action: userMemoriesExperiences.action,
125
+ capturedAt: userMemoriesExperiences.capturedAt,
126
+ createdAt: userMemoriesExperiences.createdAt,
127
+ id: userMemoriesExperiences.id,
128
+ keyLearning: userMemoriesExperiences.keyLearning,
129
+ scoreConfidence: userMemoriesExperiences.scoreConfidence,
130
+ situation: userMemoriesExperiences.situation,
131
+ tags: userMemoriesExperiences.tags,
132
+ title: userMemories.title,
133
+ type: userMemoriesExperiences.type,
134
+ updatedAt: userMemoriesExperiences.updatedAt,
135
+ })
136
+ .from(userMemoriesExperiences)
137
+ .innerJoin(userMemories, joinCondition)
138
+ .where(whereClause)
139
+ .orderBy(...orderByClauses)
140
+ .limit(normalizedPageSize)
141
+ .offset(offset),
142
+ this.db
143
+ .select({ count: sql<number>`COUNT(*)::int` })
144
+ .from(userMemoriesExperiences)
145
+ .innerJoin(userMemories, joinCondition)
146
+ .where(whereClause),
147
+ ]);
148
+
149
+ return {
150
+ items: rows,
151
+ page: normalizedPage,
152
+ pageSize: normalizedPageSize,
153
+ total: Number(totalResult[0]?.count ?? 0),
154
+ };
155
+ };
156
+
67
157
  findById = async (id: string) => {
68
158
  return this.db.query.userMemoriesExperiences.findFirst({
69
159
  where: and(
@@ -1,5 +1,6 @@
1
+ import type { IdentityListParams, IdentityListResult } from '@lobechat/types';
1
2
  import { RelationshipEnum } from '@lobechat/types';
2
- import { and, desc, eq, isNull, or } from 'drizzle-orm';
3
+ import { type SQL, and, asc, desc, eq, ilike, inArray, isNull, or, sql } from 'drizzle-orm';
3
4
 
4
5
  import {
5
6
  NewUserMemoryIdentity,
@@ -60,11 +61,101 @@ export class UserMemoryIdentityModel {
60
61
  query = async (limit = 50) => {
61
62
  return this.db.query.userMemoriesIdentities.findMany({
62
63
  limit,
63
- orderBy: [desc(userMemoriesIdentities.createdAt)],
64
+ orderBy: [desc(userMemoriesIdentities.capturedAt)],
64
65
  where: eq(userMemoriesIdentities.userId, this.userId),
65
66
  });
66
67
  };
67
68
 
69
+ /**
70
+ * Query identity list with pagination, search, and sorting
71
+ * Returns a flat structure optimized for frontend display
72
+ */
73
+ queryList = async (params: IdentityListParams = {}): Promise<IdentityListResult> => {
74
+ const { order = 'desc', page = 1, pageSize = 20, q, relationships, sort, tags, types } = params;
75
+
76
+ const normalizedPage = Math.max(1, page);
77
+ const normalizedPageSize = Math.min(Math.max(pageSize, 1), 100);
78
+ const offset = (normalizedPage - 1) * normalizedPageSize;
79
+ const normalizedQuery = typeof q === 'string' ? q.trim() : '';
80
+
81
+ // Build WHERE conditions
82
+ const conditions: Array<SQL | undefined> = [
83
+ eq(userMemoriesIdentities.userId, this.userId),
84
+ // Full-text search across title, description, role
85
+ normalizedQuery
86
+ ? or(
87
+ ilike(userMemories.title, `%${normalizedQuery}%`),
88
+ ilike(userMemoriesIdentities.description, `%${normalizedQuery}%`),
89
+ ilike(userMemoriesIdentities.role, `%${normalizedQuery}%`),
90
+ )
91
+ : undefined,
92
+ types && types.length > 0 ? inArray(userMemoriesIdentities.type, types) : undefined,
93
+ // Default to 'self' relationship if not specified
94
+ relationships && relationships.length > 0
95
+ ? inArray(userMemoriesIdentities.relationship, relationships)
96
+ : eq(userMemoriesIdentities.relationship, RelationshipEnum.Self),
97
+ tags && tags.length > 0
98
+ ? or(...tags.map((tag) => sql<boolean>`${tag} = ANY(${userMemoriesIdentities.tags})`))
99
+ : undefined,
100
+ ];
101
+
102
+ const filters = conditions.filter((condition): condition is SQL => condition !== undefined);
103
+ const whereClause = filters.length > 0 ? and(...filters) : undefined;
104
+
105
+ // Build ORDER BY
106
+ const applyOrder = order === 'asc' ? asc : desc;
107
+ const sortColumn =
108
+ sort === 'type' ? userMemoriesIdentities.type : userMemoriesIdentities.capturedAt;
109
+
110
+ const orderByClauses = [
111
+ applyOrder(sortColumn),
112
+ applyOrder(userMemoriesIdentities.updatedAt),
113
+ applyOrder(userMemoriesIdentities.createdAt),
114
+ ];
115
+
116
+ // JOIN condition
117
+ const joinCondition = and(
118
+ eq(userMemories.id, userMemoriesIdentities.userMemoryId),
119
+ eq(userMemories.userId, this.userId),
120
+ );
121
+
122
+ // Execute queries in parallel
123
+ const [rows, totalResult] = await Promise.all([
124
+ this.db
125
+ .select({
126
+ capturedAt: userMemoriesIdentities.capturedAt,
127
+ createdAt: userMemoriesIdentities.createdAt,
128
+ description: userMemoriesIdentities.description,
129
+ episodicDate: userMemoriesIdentities.episodicDate,
130
+ id: userMemoriesIdentities.id,
131
+ relationship: userMemoriesIdentities.relationship,
132
+ role: userMemoriesIdentities.role,
133
+ tags: userMemoriesIdentities.tags,
134
+ title: userMemories.title,
135
+ type: userMemoriesIdentities.type,
136
+ updatedAt: userMemoriesIdentities.updatedAt,
137
+ })
138
+ .from(userMemoriesIdentities)
139
+ .innerJoin(userMemories, joinCondition)
140
+ .where(whereClause)
141
+ .orderBy(...orderByClauses)
142
+ .limit(normalizedPageSize)
143
+ .offset(offset),
144
+ this.db
145
+ .select({ count: sql<number>`COUNT(*)::int` })
146
+ .from(userMemoriesIdentities)
147
+ .innerJoin(userMemories, joinCondition)
148
+ .where(whereClause),
149
+ ]);
150
+
151
+ return {
152
+ items: rows,
153
+ page: normalizedPage,
154
+ pageSize: normalizedPageSize,
155
+ total: Number(totalResult[0]?.count ?? 0),
156
+ };
157
+ };
158
+
68
159
  findById = async (id: string) => {
69
160
  return this.db.query.userMemoriesIdentities.findFirst({
70
161
  where: and(eq(userMemoriesIdentities.id, id), eq(userMemoriesIdentities.userId, this.userId)),
@@ -311,6 +311,7 @@ export interface QueryIdentityRolesResult {
311
311
  }
312
312
 
313
313
  export type QueryUserMemoriesSort =
314
+ | 'capturedAt' // all layers
314
315
  | 'scoreConfidence' // user_memories_experiences
315
316
  | 'scoreImpact' // user_memories_contexts
316
317
  | 'scorePriority' // user_memories_preferences
@@ -668,7 +669,10 @@ export class UserMemoryModel {
668
669
  const { page = 1, size = 10 } = params;
669
670
  const offset = (page - 1) * size;
670
671
 
671
- const identityConditions = [eq(userMemoriesIdentities.userId, this.userId)];
672
+ const identityConditions = [
673
+ eq(userMemoriesIdentities.userId, this.userId),
674
+ eq(userMemoriesIdentities.relationship, RelationshipEnum.Self),
675
+ ];
672
676
 
673
677
  const identityTags = this.db.$with('identity_tags').as(
674
678
  this.db
@@ -792,7 +796,9 @@ export class UserMemoryModel {
792
796
  const scoreColumn =
793
797
  sort === 'scoreUrgency'
794
798
  ? userMemoriesContexts.scoreUrgency
795
- : userMemoriesContexts.scoreImpact;
799
+ : sort === 'scoreImpact'
800
+ ? userMemoriesContexts.scoreImpact
801
+ : userMemoriesContexts.capturedAt;
796
802
 
797
803
  const orderByClauses = buildOrderBy(
798
804
  scoreColumn,
@@ -880,8 +886,13 @@ export class UserMemoryModel {
880
886
  };
881
887
  }
882
888
  case LayersEnum.Experience: {
889
+ const scoreColumn =
890
+ sort === 'scoreConfidence'
891
+ ? userMemoriesExperiences.scoreConfidence
892
+ : userMemoriesExperiences.capturedAt;
893
+
883
894
  const orderByClauses = buildOrderBy(
884
- userMemoriesExperiences.scoreConfidence,
895
+ scoreColumn,
885
896
  userMemoriesExperiences.updatedAt,
886
897
  userMemoriesExperiences.createdAt,
887
898
  );
@@ -956,7 +967,7 @@ export class UserMemoryModel {
956
967
  case LayersEnum.Identity: {
957
968
  const orderByClauses = buildOrderBy(
958
969
  undefined,
959
- userMemoriesIdentities.updatedAt,
970
+ userMemoriesIdentities.capturedAt,
960
971
  userMemoriesIdentities.createdAt,
961
972
  );
962
973
  const joinCondition = and(
@@ -986,6 +997,7 @@ export class UserMemoryModel {
986
997
  .select({
987
998
  identity: {
988
999
  accessedAt: userMemoriesIdentities.accessedAt,
1000
+ capturedAt: userMemoriesIdentities.capturedAt,
989
1001
  createdAt: userMemoriesIdentities.createdAt,
990
1002
  description: userMemoriesIdentities.description,
991
1003
  episodicDate: userMemoriesIdentities.episodicDate,
@@ -994,6 +1006,7 @@ export class UserMemoryModel {
994
1006
  relationship: userMemoriesIdentities.relationship,
995
1007
  role: userMemoriesIdentities.role,
996
1008
  tags: userMemoriesIdentities.tags,
1009
+ title: userMemories.title,
997
1010
  type: userMemoriesIdentities.type,
998
1011
  updatedAt: userMemoriesIdentities.updatedAt,
999
1012
  userId: userMemoriesIdentities.userId,
@@ -1028,8 +1041,13 @@ export class UserMemoryModel {
1028
1041
  };
1029
1042
  }
1030
1043
  case LayersEnum.Preference: {
1044
+ const scoreColumn =
1045
+ sort === 'scorePriority'
1046
+ ? userMemoriesPreferences.scorePriority
1047
+ : userMemoriesPreferences.capturedAt;
1048
+
1031
1049
  const orderByClauses = buildOrderBy(
1032
- userMemoriesPreferences.scorePriority,
1050
+ scoreColumn,
1033
1051
  userMemoriesPreferences.updatedAt,
1034
1052
  userMemoriesPreferences.createdAt,
1035
1053
  );
@@ -1226,6 +1244,7 @@ export class UserMemoryModel {
1226
1244
  const [identity] = await this.db
1227
1245
  .select({
1228
1246
  accessedAt: userMemoriesIdentities.accessedAt,
1247
+ capturedAt: userMemoriesIdentities.capturedAt,
1229
1248
  createdAt: userMemoriesIdentities.createdAt,
1230
1249
  description: userMemoriesIdentities.description,
1231
1250
  episodicDate: userMemoriesIdentities.episodicDate,
@@ -1933,7 +1952,7 @@ export class UserMemoryModel {
1933
1952
  .select(selectNonVectorColumns(userMemoriesIdentities))
1934
1953
  .from(userMemoriesIdentities)
1935
1954
  .where(eq(userMemoriesIdentities.userId, this.userId))
1936
- .orderBy(desc(userMemoriesIdentities.createdAt));
1955
+ .orderBy(desc(userMemoriesIdentities.capturedAt));
1937
1956
 
1938
1957
  return res;
1939
1958
  };
@@ -1947,7 +1966,7 @@ export class UserMemoryModel {
1947
1966
  .from(userMemoriesIdentities)
1948
1967
  .innerJoin(userMemories, eq(userMemories.id, userMemoriesIdentities.userMemoryId))
1949
1968
  .where(eq(userMemoriesIdentities.userId, this.userId))
1950
- .orderBy(desc(userMemoriesIdentities.createdAt));
1969
+ .orderBy(desc(userMemoriesIdentities.capturedAt));
1951
1970
 
1952
1971
  return res as Array<{
1953
1972
  identity: typeof userMemoriesIdentities.$inferSelect;
@@ -1962,7 +1981,7 @@ export class UserMemoryModel {
1962
1981
  .where(
1963
1982
  and(eq(userMemoriesIdentities.userId, this.userId), eq(userMemoriesIdentities.type, type)),
1964
1983
  )
1965
- .orderBy(desc(userMemoriesIdentities.createdAt));
1984
+ .orderBy(desc(userMemoriesIdentities.capturedAt));
1966
1985
 
1967
1986
  return res;
1968
1987
  };
@@ -0,0 +1,25 @@
1
+ import type { BaseListItem, BaseListParams, BaseListResult } from './shared';
2
+
3
+ /**
4
+ * Experience query types for list display
5
+ * These are flat structures optimized for frontend rendering
6
+ */
7
+
8
+ export type ExperienceListSort = 'capturedAt' | 'scoreConfidence';
9
+
10
+ export interface ExperienceListParams extends BaseListParams {
11
+ sort?: ExperienceListSort;
12
+ }
13
+
14
+ /**
15
+ * Flat structure for experience list items
16
+ * Contains fields needed for card display, excluding detail fields like reasoning/possibleOutcome
17
+ */
18
+ export interface ExperienceListItem extends BaseListItem {
19
+ action: string | null;
20
+ keyLearning: string | null;
21
+ scoreConfidence: number | null;
22
+ situation: string | null;
23
+ }
24
+
25
+ export type ExperienceListResult = BaseListResult<ExperienceListItem>;
@@ -1,7 +1,34 @@
1
1
  import { z } from 'zod';
2
2
 
3
+ import type { BaseListItem, BaseListParams, BaseListResult } from './shared';
4
+
3
5
  export type IdentityType = 'personal' | 'professional' | 'demographic';
4
6
 
7
+ /**
8
+ * Identity query types for list display
9
+ * These are flat structures optimized for frontend rendering
10
+ */
11
+
12
+ export type IdentityListSort = 'capturedAt' | 'type';
13
+
14
+ export interface IdentityListParams extends BaseListParams {
15
+ relationships?: string[];
16
+ sort?: IdentityListSort;
17
+ }
18
+
19
+ /**
20
+ * Flat structure for identity list items
21
+ * Contains fields needed for card display
22
+ */
23
+ export interface IdentityListItem extends BaseListItem {
24
+ description: string | null;
25
+ episodicDate: Date | null;
26
+ relationship: string | null;
27
+ role: string | null;
28
+ }
29
+
30
+ export type IdentityListResult = BaseListResult<IdentityListItem>;
31
+
5
32
  export interface UserMemoryIdentity {
6
33
  accessedAt: Date;
7
34
  createdAt: Date;
@@ -1,4 +1,5 @@
1
1
  export * from './base';
2
+ export * from './experience';
2
3
  export * from './identity';
3
4
  export * from './layers';
4
5
  export * from './list';