opencode-mem 2.0.1 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.md +54 -11
  2. package/dist/config.d.ts +5 -0
  3. package/dist/config.d.ts.map +1 -1
  4. package/dist/config.js +62 -6
  5. package/dist/index.d.ts.map +1 -1
  6. package/dist/index.js +18 -11
  7. package/dist/plugin.d.ts.map +1 -1
  8. package/dist/plugin.js +0 -4
  9. package/dist/services/ai/providers/anthropic-messages.d.ts.map +1 -1
  10. package/dist/services/ai/providers/anthropic-messages.js +11 -17
  11. package/dist/services/ai/providers/openai-chat-completion.d.ts.map +1 -1
  12. package/dist/services/ai/providers/openai-chat-completion.js +11 -17
  13. package/dist/services/ai/providers/openai-responses.d.ts.map +1 -1
  14. package/dist/services/ai/providers/openai-responses.js +11 -17
  15. package/dist/services/api-handlers.d.ts +4 -0
  16. package/dist/services/api-handlers.d.ts.map +1 -1
  17. package/dist/services/api-handlers.js +155 -1
  18. package/dist/services/auto-capture.js +2 -2
  19. package/dist/services/client.d.ts +1 -16
  20. package/dist/services/client.d.ts.map +1 -1
  21. package/dist/services/client.js +0 -35
  22. package/dist/services/context.d.ts +1 -7
  23. package/dist/services/context.d.ts.map +1 -1
  24. package/dist/services/context.js +6 -14
  25. package/dist/services/deduplication-service.d.ts.map +1 -1
  26. package/dist/services/deduplication-service.js +1 -3
  27. package/dist/services/logger.js +1 -1
  28. package/dist/services/user-memory-learning.d.ts.map +1 -1
  29. package/dist/services/user-memory-learning.js +122 -84
  30. package/dist/services/user-profile/profile-context.d.ts +2 -0
  31. package/dist/services/user-profile/profile-context.d.ts.map +1 -0
  32. package/dist/services/user-profile/profile-context.js +40 -0
  33. package/dist/services/user-profile/types.d.ts +51 -0
  34. package/dist/services/user-profile/types.d.ts.map +1 -0
  35. package/dist/services/user-profile/types.js +1 -0
  36. package/dist/services/user-profile/user-profile-manager.d.ts +22 -0
  37. package/dist/services/user-profile/user-profile-manager.d.ts.map +1 -0
  38. package/dist/services/user-profile/user-profile-manager.js +267 -0
  39. package/dist/services/web-server-worker.js +29 -1
  40. package/dist/types/index.d.ts +1 -3
  41. package/dist/types/index.d.ts.map +1 -1
  42. package/dist/web/app.js +249 -29
  43. package/dist/web/index.html +31 -5
  44. package/dist/web/styles.css +309 -0
  45. package/package.json +1 -1
@@ -149,7 +149,40 @@ export async function handleListMemories(tag, page = 1, pageSize = 20, scope, in
149
149
  }));
150
150
  timeline = [...memoriesWithType, ...promptsWithType];
151
151
  }
152
- timeline.sort((a, b) => b.createdAt - a.createdAt);
152
+ const linkedPairs = new Map();
153
+ const standalone = [];
154
+ for (const item of timeline) {
155
+ if (item.type === "memory" && item.linkedPromptId) {
156
+ if (!linkedPairs.has(item.linkedPromptId)) {
157
+ linkedPairs.set(item.linkedPromptId, { memory: item, prompt: null });
158
+ }
159
+ else {
160
+ linkedPairs.get(item.linkedPromptId).memory = item;
161
+ }
162
+ }
163
+ else if (item.type === "prompt" && item.linkedMemoryId) {
164
+ if (!linkedPairs.has(item.id)) {
165
+ linkedPairs.set(item.id, { memory: null, prompt: item });
166
+ }
167
+ else {
168
+ linkedPairs.get(item.id).prompt = item;
169
+ }
170
+ }
171
+ else {
172
+ standalone.push(item);
173
+ }
174
+ }
175
+ const sortedTimeline = [];
176
+ const pairs = Array.from(linkedPairs.values())
177
+ .filter((p) => p.memory && p.prompt)
178
+ .sort((a, b) => b.memory.createdAt - a.memory.createdAt);
179
+ for (const pair of pairs) {
180
+ sortedTimeline.push(pair.memory);
181
+ sortedTimeline.push(pair.prompt);
182
+ }
183
+ standalone.sort((a, b) => b.createdAt - a.createdAt);
184
+ sortedTimeline.push(...standalone);
185
+ timeline = sortedTimeline;
153
186
  const total = timeline.length;
154
187
  const totalPages = Math.ceil(total / pageSize);
155
188
  const offset = (page - 1) * pageSize;
@@ -211,6 +244,12 @@ export async function handleAddMemory(data) {
211
244
  await embeddingService.warmup();
212
245
  const vector = await embeddingService.embedWithTimeout(data.content);
213
246
  const { scope, hash } = extractScopeFromTag(data.containerTag);
247
+ if (scope === "user") {
248
+ return {
249
+ success: false,
250
+ error: "User-scoped memories are deprecated. Use user profile system instead.",
251
+ };
252
+ }
214
253
  const shard = shardManager.getWriteShard(scope, hash);
215
254
  const id = `mem_${Date.now()}_${Math.random().toString(36).substring(2, 11)}`;
216
255
  const now = Date.now();
@@ -605,3 +644,118 @@ export async function handleBulkDeletePrompts(ids, cascade = false) {
605
644
  return { success: false, error: String(error) };
606
645
  }
607
646
  }
647
+ export async function handleGetUserProfile(userId) {
648
+ try {
649
+ const { userProfileManager } = await import("./user-profile/user-profile-manager.js");
650
+ const { getTags } = await import("./tags.js");
651
+ let targetUserId = userId;
652
+ if (!targetUserId) {
653
+ const tags = getTags(process.cwd());
654
+ targetUserId = tags.user.userEmail || "unknown";
655
+ }
656
+ const profile = userProfileManager.getActiveProfile(targetUserId);
657
+ if (!profile) {
658
+ return {
659
+ success: true,
660
+ data: {
661
+ exists: false,
662
+ userId: targetUserId,
663
+ message: "No profile found. Keep chatting to build your profile.",
664
+ },
665
+ };
666
+ }
667
+ const profileData = JSON.parse(profile.profileData);
668
+ return {
669
+ success: true,
670
+ data: {
671
+ exists: true,
672
+ id: profile.id,
673
+ userId: profile.userId,
674
+ displayName: profile.displayName,
675
+ userName: profile.userName,
676
+ userEmail: profile.userEmail,
677
+ version: profile.version,
678
+ createdAt: safeToISOString(profile.createdAt),
679
+ lastAnalyzedAt: safeToISOString(profile.lastAnalyzedAt),
680
+ totalPromptsAnalyzed: profile.totalPromptsAnalyzed,
681
+ profileData,
682
+ },
683
+ };
684
+ }
685
+ catch (error) {
686
+ log("handleGetUserProfile: error", { error: String(error) });
687
+ return { success: false, error: String(error) };
688
+ }
689
+ }
690
+ export async function handleGetProfileChangelog(profileId, limit = 5) {
691
+ try {
692
+ if (!profileId) {
693
+ return { success: false, error: "profileId is required" };
694
+ }
695
+ const { userProfileManager } = await import("./user-profile/user-profile-manager.js");
696
+ const changelogs = userProfileManager.getProfileChangelogs(profileId, limit);
697
+ const formattedChangelogs = changelogs.map((c) => ({
698
+ id: c.id,
699
+ profileId: c.profileId,
700
+ version: c.version,
701
+ changeType: c.changeType,
702
+ changeSummary: c.changeSummary,
703
+ createdAt: safeToISOString(c.createdAt),
704
+ }));
705
+ return { success: true, data: formattedChangelogs };
706
+ }
707
+ catch (error) {
708
+ log("handleGetProfileChangelog: error", { error: String(error) });
709
+ return { success: false, error: String(error) };
710
+ }
711
+ }
712
+ export async function handleGetProfileSnapshot(changelogId) {
713
+ try {
714
+ if (!changelogId) {
715
+ return { success: false, error: "changelogId is required" };
716
+ }
717
+ const { userProfileManager } = await import("./user-profile/user-profile-manager.js");
718
+ const changelogs = userProfileManager.getProfileChangelogs("", 1000);
719
+ const changelog = changelogs.find((c) => c.id === changelogId);
720
+ if (!changelog) {
721
+ return { success: false, error: "Changelog not found" };
722
+ }
723
+ const profileData = JSON.parse(changelog.profileDataSnapshot);
724
+ return {
725
+ success: true,
726
+ data: {
727
+ version: changelog.version,
728
+ createdAt: safeToISOString(changelog.createdAt),
729
+ profileData,
730
+ },
731
+ };
732
+ }
733
+ catch (error) {
734
+ log("handleGetProfileSnapshot: error", { error: String(error) });
735
+ return { success: false, error: String(error) };
736
+ }
737
+ }
738
+ export async function handleRefreshProfile(userId) {
739
+ try {
740
+ const { getTags } = await import("./tags.js");
741
+ const { userPromptManager } = await import("./user-prompt/user-prompt-manager.js");
742
+ let targetUserId = userId;
743
+ if (!targetUserId) {
744
+ const tags = getTags(process.cwd());
745
+ targetUserId = tags.user.userEmail || "unknown";
746
+ }
747
+ const unanalyzedCount = userPromptManager.countUnanalyzedForUserLearning();
748
+ return {
749
+ success: true,
750
+ data: {
751
+ message: "Profile refresh queued",
752
+ unanalyzedPrompts: unanalyzedCount,
753
+ note: "Profile will be updated when threshold is reached",
754
+ },
755
+ };
756
+ }
757
+ catch (error) {
758
+ log("handleRefreshProfile: error", { error: String(error) });
759
+ return { success: false, error: String(error) };
760
+ }
761
+ }
@@ -37,7 +37,7 @@ export async function performAutoCapture(ctx, sessionID, directory) {
37
37
  const tags = getTags(directory);
38
38
  const latestMemory = await getLatestProjectMemory(tags.project.tag);
39
39
  const context = buildMarkdownContext(prompt.content, textResponses, toolCalls, latestMemory);
40
- const summaryResult = await generateSummary(ctx, context, sessionID);
40
+ const summaryResult = await generateSummary(context, sessionID);
41
41
  if (!summaryResult || summaryResult.type === "skip") {
42
42
  log("Auto-capture: skipped non-technical conversation", { sessionID });
43
43
  userPromptManager.deletePrompt(prompt.id);
@@ -179,7 +179,7 @@ function buildMarkdownContext(userPrompt, textResponses, toolCalls, latestMemory
179
179
  }
180
180
  return sections.join("\n");
181
181
  }
182
- async function generateSummary(ctx, context, sessionID) {
182
+ async function generateSummary(context, sessionID) {
183
183
  if (!CONFIG.memoryModel || !CONFIG.memoryApiUrl || !CONFIG.memoryApiKey) {
184
184
  throw new Error("External API not configured for auto-capture");
185
185
  }
@@ -1,9 +1,4 @@
1
1
  import type { MemoryType } from "../types/index.js";
2
- import type { SearchResult } from "./sqlite/types.js";
3
- interface ProfileData {
4
- static: string[];
5
- dynamic: string[];
6
- }
7
2
  export declare class LocalMemoryClient {
8
3
  private initPromise;
9
4
  private isInitialized;
@@ -19,7 +14,7 @@ export declare class LocalMemoryClient {
19
14
  close(): void;
20
15
  searchMemories(query: string, containerTag: string): Promise<{
21
16
  success: true;
22
- results: SearchResult[];
17
+ results: import("./sqlite/types.js").SearchResult[];
23
18
  total: number;
24
19
  timing: number;
25
20
  error?: undefined;
@@ -30,15 +25,6 @@ export declare class LocalMemoryClient {
30
25
  total: number;
31
26
  timing: number;
32
27
  }>;
33
- getProfile(containerTag: string, query?: string): Promise<{
34
- success: true;
35
- profile: ProfileData;
36
- error?: undefined;
37
- } | {
38
- success: false;
39
- error: string;
40
- profile: null;
41
- }>;
42
28
  addMemory(content: string, containerTag: string, metadata?: {
43
29
  type?: MemoryType;
44
30
  source?: "manual" | "auto-capture" | "import" | "api";
@@ -101,5 +87,4 @@ export declare class LocalMemoryClient {
101
87
  }>;
102
88
  }
103
89
  export declare const memoryClient: LocalMemoryClient;
104
- export {};
105
90
  //# sourceMappingURL=client.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/services/client.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AACpD,OAAO,KAAK,EAAgB,YAAY,EAAE,MAAM,mBAAmB,CAAC;AA2CpE,UAAU,WAAW;IACnB,MAAM,EAAE,MAAM,EAAE,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB;AAED,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,aAAa,CAAkB;;YAIzB,UAAU;IAiBlB,MAAM,CAAC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjE,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAIjC,SAAS,IAAI;QACX,WAAW,EAAE,OAAO,CAAC;QACrB,WAAW,EAAE,OAAO,CAAC;QACrB,KAAK,EAAE,OAAO,CAAC;KAChB;IAQD,KAAK,IAAI,IAAI;IAIP,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;;;;;;;;;;;;;IA6BlD,UAAU,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,MAAM;;;;;;;;;IAyC/C,SAAS,CACb,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,UAAU,CAAC;QAClB,MAAM,CAAC,EAAE,QAAQ,GAAG,cAAc,GAAG,QAAQ,GAAG,KAAK,CAAC;QACtD,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB;;;;;;;;;IAqDG,YAAY,CAAC,QAAQ,EAAE,MAAM;;;;;;;IA2B7B,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,SAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuDpD;AAED,eAAO,MAAM,YAAY,mBAA0B,CAAC"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/services/client.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AA4CpD,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,WAAW,CAA8B;IACjD,OAAO,CAAC,aAAa,CAAkB;;YAIzB,UAAU;IAiBlB,MAAM,CAAC,gBAAgB,CAAC,EAAE,CAAC,QAAQ,EAAE,GAAG,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjE,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAIjC,SAAS,IAAI;QACX,WAAW,EAAE,OAAO,CAAC;QACrB,WAAW,EAAE,OAAO,CAAC;QACrB,KAAK,EAAE,OAAO,CAAC;KAChB;IAQD,KAAK,IAAI,IAAI;IAIP,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM;;;;;;;;;;;;;IA6BlD,SAAS,CACb,OAAO,EAAE,MAAM,EACf,YAAY,EAAE,MAAM,EACpB,QAAQ,CAAC,EAAE;QACT,IAAI,CAAC,EAAE,UAAU,CAAC;QAClB,MAAM,CAAC,EAAE,QAAQ,GAAG,cAAc,GAAG,QAAQ,GAAG,KAAK,CAAC;QACtD,IAAI,CAAC,EAAE,MAAM,CAAC;QACd,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,gBAAgB,CAAC,EAAE,MAAM,CAAC;QAC1B,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,WAAW,CAAC,EAAE,MAAM,CAAC;QACrB,UAAU,CAAC,EAAE,MAAM,CAAC;QACpB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;KACxB;;;;;;;;;IAqDG,YAAY,CAAC,QAAQ,EAAE,MAAM;;;;;;;IA2B7B,YAAY,CAAC,YAAY,EAAE,MAAM,EAAE,KAAK,SAAK;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAuDpD;AAED,eAAO,MAAM,YAAY,mBAA0B,CAAC"}
@@ -96,41 +96,6 @@ export class LocalMemoryClient {
96
96
  return { success: false, error: errorMessage, results: [], total: 0, timing: 0 };
97
97
  }
98
98
  }
99
- async getProfile(containerTag, query) {
100
- try {
101
- await this.initialize();
102
- const { scope, hash } = extractScopeFromContainerTag(containerTag);
103
- const shards = shardManager.getAllShards(scope, hash);
104
- if (shards.length === 0) {
105
- log("getProfile: no shards found", { containerTag });
106
- return { success: true, profile: { static: [], dynamic: [] } };
107
- }
108
- const staticFacts = [];
109
- const dynamicFacts = [];
110
- for (const shard of shards) {
111
- const db = connectionManager.getConnection(shard.dbPath);
112
- const memories = vectorSearch.listMemories(db, containerTag, CONFIG.maxProfileItems * 2);
113
- for (const m of memories) {
114
- if (m.type === "preference") {
115
- staticFacts.push(m.content);
116
- }
117
- else {
118
- dynamicFacts.push(m.content);
119
- }
120
- }
121
- }
122
- const profile = {
123
- static: staticFacts.slice(0, CONFIG.maxProfileItems),
124
- dynamic: dynamicFacts.slice(0, CONFIG.maxProfileItems),
125
- };
126
- return { success: true, profile };
127
- }
128
- catch (error) {
129
- const errorMessage = error instanceof Error ? error.message : String(error);
130
- log("getProfile: error", { error: errorMessage });
131
- return { success: false, error: errorMessage, profile: null };
132
- }
133
- }
134
99
  async addMemory(content, containerTag, metadata) {
135
100
  try {
136
101
  await this.initialize();
@@ -6,12 +6,6 @@ interface MemoryResultMinimal {
6
6
  interface MemoriesResponseMinimal {
7
7
  results?: MemoryResultMinimal[];
8
8
  }
9
- interface ProfileResponse {
10
- profile?: {
11
- static: string[];
12
- dynamic: string[];
13
- };
14
- }
15
- export declare function formatContextForPrompt(profile: ProfileResponse | null, userMemories: MemoriesResponseMinimal, projectMemories: MemoriesResponseMinimal): string;
9
+ export declare function formatContextForPrompt(userId: string | null, userMemories: MemoriesResponseMinimal, projectMemories: MemoriesResponseMinimal): string;
16
10
  export {};
17
11
  //# sourceMappingURL=context.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/services/context.ts"],"names":[],"mappings":"AAEA,UAAU,mBAAmB;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,uBAAuB;IAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,UAAU,eAAe;IACvB,OAAO,CAAC,EAAE;QACR,MAAM,EAAE,MAAM,EAAE,CAAC;QACjB,OAAO,EAAE,MAAM,EAAE,CAAC;KACnB,CAAC;CACH;AAED,wBAAgB,sBAAsB,CACpC,OAAO,EAAE,eAAe,GAAG,IAAI,EAC/B,YAAY,EAAE,uBAAuB,EACrC,eAAe,EAAE,uBAAuB,GACvC,MAAM,CA8CR"}
1
+ {"version":3,"file":"context.d.ts","sourceRoot":"","sources":["../../src/services/context.ts"],"names":[],"mappings":"AAGA,UAAU,mBAAmB;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,UAAU,uBAAuB;IAC/B,OAAO,CAAC,EAAE,mBAAmB,EAAE,CAAC;CACjC;AAED,wBAAgB,sBAAsB,CACpC,MAAM,EAAE,MAAM,GAAG,IAAI,EACrB,YAAY,EAAE,uBAAuB,EACrC,eAAe,EAAE,uBAAuB,GACvC,MAAM,CAmCR"}
@@ -1,19 +1,11 @@
1
1
  import { CONFIG } from "../config.js";
2
- export function formatContextForPrompt(profile, userMemories, projectMemories) {
2
+ import { getUserProfileContext } from "./user-profile/profile-context.js";
3
+ export function formatContextForPrompt(userId, userMemories, projectMemories) {
3
4
  const parts = ["[MEMORY]"];
4
- if (CONFIG.injectProfile && profile?.profile) {
5
- const { static: staticFacts, dynamic: dynamicFacts } = profile.profile;
6
- if (staticFacts.length > 0) {
7
- parts.push("\nUser Profile:");
8
- staticFacts.slice(0, CONFIG.maxProfileItems).forEach((fact) => {
9
- parts.push(`- ${fact}`);
10
- });
11
- }
12
- if (dynamicFacts.length > 0) {
13
- parts.push("\nRecent Context:");
14
- dynamicFacts.slice(0, CONFIG.maxProfileItems).forEach((fact) => {
15
- parts.push(`- ${fact}`);
16
- });
5
+ if (CONFIG.injectProfile && userId) {
6
+ const profileContext = getUserProfileContext(userId);
7
+ if (profileContext) {
8
+ parts.push("\n" + profileContext);
17
9
  }
18
10
  }
19
11
  const projectResults = projectMemories.results || [];
@@ -1 +1 @@
1
- {"version":3,"file":"deduplication-service.d.ts","sourceRoot":"","sources":["../../src/services/deduplication-service.ts"],"names":[],"mappings":"AAOA,UAAU,cAAc;IACtB,cAAc,EAAE;QACd,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,UAAU,EAAE,KAAK,CAAC;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;CACJ;AAED,UAAU,mBAAmB;IAC3B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,mBAAmB,EAAE,cAAc,EAAE,CAAC;CACvC;AAED,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,SAAS,CAAkB;IAE7B,yBAAyB,IAAI,OAAO,CAAC,mBAAmB,CAAC;IAgH/D,OAAO,CAAC,gBAAgB;IAoBxB,SAAS;;;;;CAOV;AAED,eAAO,MAAM,oBAAoB,sBAA6B,CAAC"}
1
+ {"version":3,"file":"deduplication-service.d.ts","sourceRoot":"","sources":["../../src/services/deduplication-service.ts"],"names":[],"mappings":"AAMA,UAAU,cAAc;IACtB,cAAc,EAAE;QACd,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,YAAY,EAAE,MAAM,CAAC;QACrB,SAAS,EAAE,MAAM,CAAC;KACnB,CAAC;IACF,UAAU,EAAE,KAAK,CAAC;QAChB,EAAE,EAAE,MAAM,CAAC;QACX,OAAO,EAAE,MAAM,CAAC;QAChB,UAAU,EAAE,MAAM,CAAC;KACpB,CAAC,CAAC;CACJ;AAED,UAAU,mBAAmB;IAC3B,sBAAsB,EAAE,MAAM,CAAC;IAC/B,mBAAmB,EAAE,cAAc,EAAE,CAAC;CACvC;AAED,qBAAa,oBAAoB;IAC/B,OAAO,CAAC,SAAS,CAAkB;IAE7B,yBAAyB,IAAI,OAAO,CAAC,mBAAmB,CAAC;IA+G/D,OAAO,CAAC,gBAAgB;IAoBxB,SAAS;;;;;CAOV;AAED,eAAO,MAAM,oBAAoB,sBAA6B,CAAC"}
@@ -1,4 +1,3 @@
1
- import { embeddingService } from "./embedding.js";
2
1
  import { shardManager } from "./sqlite/shard-manager.js";
3
2
  import { vectorSearch } from "./sqlite/vector-search.js";
4
3
  import { connectionManager } from "./sqlite/connection-manager.js";
@@ -34,10 +33,9 @@ export class DeduplicationService {
34
33
  }
35
34
  contentMap.get(key).push(memory);
36
35
  }
37
- for (const [key, duplicates] of contentMap) {
36
+ for (const [, duplicates] of contentMap) {
38
37
  if (duplicates.length > 1) {
39
38
  duplicates.sort((a, b) => Number(b.created_at) - Number(a.created_at));
40
- const keep = duplicates[0];
41
39
  const toDelete = duplicates.slice(1);
42
40
  for (const dup of toDelete) {
43
41
  try {
@@ -1,6 +1,6 @@
1
1
  import { appendFileSync, writeFileSync, existsSync, mkdirSync } from "fs";
2
2
  import { homedir } from "os";
3
- import { join, dirname } from "path";
3
+ import { join } from "path";
4
4
  const LOG_DIR = join(homedir(), ".opencode-mem");
5
5
  const LOG_FILE = join(LOG_DIR, "opencode-mem.log");
6
6
  if (!existsSync(LOG_DIR)) {
@@ -1 +1 @@
1
- {"version":3,"file":"user-memory-learning.d.ts","sourceRoot":"","sources":["../../src/services/user-memory-learning.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAQvD,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,WAAW,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CAyEf"}
1
+ {"version":3,"file":"user-memory-learning.d.ts","sourceRoot":"","sources":["../../src/services/user-memory-learning.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AASvD,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,WAAW,EAChB,SAAS,EAAE,MAAM,GAChB,OAAO,CAAC,IAAI,CAAC,CA8Ef"}
@@ -1,8 +1,8 @@
1
- import { memoryClient } from "./client.js";
2
1
  import { getTags } from "./tags.js";
3
2
  import { log } from "./logger.js";
4
3
  import { CONFIG } from "../config.js";
5
4
  import { userPromptManager } from "./user-prompt/user-prompt-manager.js";
5
+ import { userProfileManager } from "./user-profile/user-profile-manager.js";
6
6
  export async function performUserMemoryLearning(ctx, directory) {
7
7
  try {
8
8
  const count = userPromptManager.countUnanalyzedForUserLearning();
@@ -14,50 +14,41 @@ export async function performUserMemoryLearning(ctx, directory) {
14
14
  if (prompts.length === 0) {
15
15
  return;
16
16
  }
17
- const context = buildUserAnalysisContext(prompts);
18
- const memories = await analyzeUserPatterns(ctx, context);
19
- if (!memories || memories.length === 0) {
20
- log("User memory learning: no patterns identified", { promptCount: prompts.length });
17
+ const tags = getTags(directory);
18
+ const userId = tags.user.userEmail || "unknown";
19
+ const existingProfile = userProfileManager.getActiveProfile(userId);
20
+ const context = buildUserAnalysisContext(prompts, existingProfile);
21
+ const updatedProfileData = await analyzeUserProfile(context, existingProfile);
22
+ if (!updatedProfileData) {
23
+ log("User memory learning: no profile updates", { promptCount: prompts.length });
21
24
  userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
22
25
  return;
23
26
  }
24
- const tags = getTags(directory);
25
- let savedCount = 0;
26
- for (const memory of memories) {
27
- const result = await memoryClient.addMemory(memory.summary, tags.user.tag, {
28
- type: memory.type,
29
- source: "user-learning",
30
- promptCount: prompts.length,
31
- analysisTimestamp: Date.now(),
32
- reasoning: memory.reasoning,
33
- displayName: tags.user.displayName,
34
- userName: tags.user.userName,
35
- userEmail: tags.user.userEmail,
36
- });
37
- if (result.success) {
38
- savedCount++;
39
- }
27
+ if (existingProfile) {
28
+ const changeSummary = generateChangeSummary(JSON.parse(existingProfile.profileData), updatedProfileData);
29
+ userProfileManager.updateProfile(existingProfile.id, updatedProfileData, prompts.length, changeSummary);
40
30
  }
41
- userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
42
- if (savedCount > 0) {
43
- await ctx.client?.tui
44
- .showToast({
45
- body: {
46
- title: "User Memory Learning",
47
- message: `Learned ${savedCount} pattern${savedCount > 1 ? "s" : ""} from ${prompts.length} prompts`,
48
- variant: "success",
49
- duration: 3000,
50
- },
51
- })
52
- .catch(() => { });
31
+ else {
32
+ userProfileManager.createProfile(userId, tags.user.displayName || "Unknown", tags.user.userName || "unknown", tags.user.userEmail || "unknown", updatedProfileData, prompts.length);
53
33
  }
34
+ userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
35
+ await ctx.client?.tui
36
+ .showToast({
37
+ body: {
38
+ title: "User Profile Updated",
39
+ message: `Analyzed ${prompts.length} prompts and updated your profile`,
40
+ variant: "success",
41
+ duration: 3000,
42
+ },
43
+ })
44
+ .catch(() => { });
54
45
  }
55
46
  catch (error) {
56
47
  log("User memory learning error", { error: String(error) });
57
48
  await ctx.client?.tui
58
49
  .showToast({
59
50
  body: {
60
- title: "User Memory Learning Failed",
51
+ title: "User Profile Update Failed",
61
52
  message: String(error),
62
53
  variant: "error",
63
54
  duration: 5000,
@@ -66,37 +57,63 @@ export async function performUserMemoryLearning(ctx, directory) {
66
57
  .catch(() => { });
67
58
  }
68
59
  }
69
- function buildUserAnalysisContext(prompts) {
70
- return `# User Prompt History Analysis
60
+ function generateChangeSummary(oldProfile, newProfile) {
61
+ const changes = [];
62
+ const prefDiff = newProfile.preferences.length - oldProfile.preferences.length;
63
+ if (prefDiff > 0)
64
+ changes.push(`+${prefDiff} preferences`);
65
+ const patternDiff = newProfile.patterns.length - oldProfile.patterns.length;
66
+ if (patternDiff > 0)
67
+ changes.push(`+${patternDiff} patterns`);
68
+ const workflowDiff = newProfile.workflows.length - oldProfile.workflows.length;
69
+ if (workflowDiff > 0)
70
+ changes.push(`+${workflowDiff} workflows`);
71
+ return changes.length > 0 ? changes.join(", ") : "Profile refinement";
72
+ }
73
+ function buildUserAnalysisContext(prompts, existingProfile) {
74
+ const existingProfileSection = existingProfile
75
+ ? `
76
+ ## Existing User Profile
77
+
78
+ ${existingProfile.profileData}
71
79
 
72
- Analyze the following ${prompts.length} user prompts to identify patterns, preferences, and workflows.
80
+ **Instructions**: Merge new insights with the existing profile. Update confidence scores for reinforced patterns, add new patterns, and refine existing ones.`
81
+ : `
82
+ **Instructions**: Create a new user profile from scratch based on the prompts below.`;
83
+ return `# User Profile Analysis
73
84
 
74
- ## Prompts
85
+ Analyze ${prompts.length} user prompts to ${existingProfile ? "update" : "create"} the user profile.
86
+
87
+ ${existingProfileSection}
88
+
89
+ ## Recent Prompts
75
90
 
76
91
  ${prompts.map((p, i) => `${i + 1}. ${p.content}`).join("\n\n")}
77
92
 
78
- ## Analysis Instructions
93
+ ## Analysis Guidelines
79
94
 
80
- Identify user patterns and preferences from these prompts. Look for:
95
+ Identify and ${existingProfile ? "update" : "create"}:
81
96
 
82
- 1. **Preferences**: How the user likes things done
83
- - Code style preferences (e.g., "prefers code without comments")
84
- - Communication preferences (e.g., "likes concise responses")
85
- - Tool preferences (e.g., "prefers TypeScript over JavaScript")
97
+ 1. **Preferences** (max ${CONFIG.userProfileMaxPreferences})
98
+ - Code style, communication style, tool preferences
99
+ - Assign confidence 0.5-1.0 based on evidence strength
100
+ - Include 1-3 example prompts as evidence
86
101
 
87
- 2. **Patterns**: Recurring topics or requests
88
- - Technical topics (e.g., "often asks about database optimization")
89
- - Problem domains (e.g., "frequently works on authentication")
90
- - Skill level indicators (e.g., "asks detailed questions about async patterns")
102
+ 2. **Patterns** (max ${CONFIG.userProfileMaxPatterns})
103
+ - Recurring topics, problem domains, technical interests
104
+ - Track frequency of occurrence
91
105
 
92
- 3. **Workflows**n sequences or habits
93
- - Development flow (e.g., "usually asks for tests after implementation")
94
- - Review habits (e.g., "always requests code review before committing")
95
- - Learning style (e.g., "prefers examples over explanations")
106
+ 3. **Workflows** (max ${CONFIG.userProfileMaxWorkflows})
107
+ - Development sequences, habits, learning style
108
+ - Break down into steps if applicable
96
109
 
97
- Generate 1-5 key insights as memories. Only include patterns that are clearly evident from multiple prompts.`;
110
+ 4. **Skill Level**
111
+ - Overall: beginner/intermediate/advanced
112
+ - Per-domain assessment (e.g., typescript: advanced, docker: beginner)
113
+
114
+ ${existingProfile ? "Merge with existing profile, incrementing frequencies and updating confidence scores." : "Create initial profile with conservative confidence scores."}`;
98
115
  }
99
- async function analyzeUserPatterns(ctx, context) {
116
+ async function analyzeUserProfile(context, existingProfile) {
100
117
  if (!CONFIG.memoryModel || !CONFIG.memoryApiUrl || !CONFIG.memoryApiKey) {
101
118
  throw new Error("External API not configured for user memory learning");
102
119
  }
@@ -111,54 +128,75 @@ async function analyzeUserPatterns(ctx, context) {
111
128
  const provider = AIProviderFactory.createProvider(CONFIG.memoryProvider, providerConfig);
112
129
  const systemPrompt = `You are a user behavior analyst for a coding assistant.
113
130
 
114
- Your task is to analyze user prompts fy patterns, preferences, and workflows.
115
-
116
- Use the save_user_memories tool to save identified patterns. Only save patterns that are clearly evident from the prompts.
131
+ Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile.
117
132
 
118
- IMPORTANT: All memories must have scope set to "user".`;
133
+ Use the update_user_profile tool to save the ${existingProfile ? "updated" : "new"} profile.`;
119
134
  const toolSchema = {
120
135
  type: "function",
121
136
  function: {
122
- name: "save_user_memories",
123
- description: "Save identified user patterns and preferences",
137
+ name: "update_user_profile",
138
+ description: existingProfile
139
+ ? "Update existing user profile with new insights"
140
+ : "Create new user profile",
124
141
  parameters: {
125
142
  type: "object",
126
143
  properties: {
127
- memories: {
144
+ preferences: {
128
145
  type: "array",
129
- description: "Array of identified patterns (1-5 memories)",
130
146
  items: {
131
147
  type: "object",
132
148
  properties: {
133
- summary: {
134
- type: "string",
135
- description: "Clear description of the pattern or preference",
136
- },
137
- scope: {
138
- type: "string",
139
- enum: ["user"],
140
- description: "Memory scope (must be 'user' for user learning)",
141
- },
142
- type: {
143
- type: "string",
144
- description: "Type of insight (e.g., preference, pattern, workflow, skill-level, communication-style)",
145
- },
146
- reasoning: {
147
- type: "string",
148
- description: "Why this pattern is significant (optial)",
149
- },
149
+ category: { type: "string" },
150
+ description: { type: "string" },
151
+ confidence: { type: "number", minimum: 0, maximum: 1 },
152
+ evidence: { type: "array", items: { type: "string" }, maxItems: 3 },
150
153
  },
151
- required: ["summary", "scope", "type"],
154
+ required: ["category", "description", "confidence", "evidence"],
152
155
  },
153
156
  },
157
+ patterns: {
158
+ type: "array",
159
+ items: {
160
+ type: "object",
161
+ properties: {
162
+ category: { type: "string" },
163
+ description: { type: "string" },
164
+ },
165
+ required: ["category", "description"],
166
+ },
167
+ },
168
+ workflows: {
169
+ type: "array",
170
+ items: {
171
+ type: "object",
172
+ properties: {
173
+ description: { type: "string" },
174
+ steps: { type: "array", items: { type: "string" } },
175
+ },
176
+ required: ["description", "steps"],
177
+ },
178
+ },
179
+ skillLevel: {
180
+ type: "object",
181
+ properties: {
182
+ overall: { type: "string", enum: ["beginner", "intermediate", "advanced"] },
183
+ domains: { type: "object", additionalProperties: { type: "string" } },
184
+ },
185
+ required: ["overall", "domains"],
186
+ },
154
187
  },
155
- required: ["memories"],
188
+ required: ["preferences", "patterns", "workflows", "skillLevel"],
156
189
  },
157
190
  },
158
191
  };
159
- const result = await provider.executeToolCall(systemPrompt, context, toolSchema, "user-learning");
192
+ const result = await provider.executeToolCall(systemPrompt, context, toolSchema, "user-profile");
160
193
  if (!result.success || !result.data) {
161
- throw new Error(result.error || "Failed to analyze user patterns");
194
+ throw new Error(result.error || "Failed to analyze user profile");
195
+ }
196
+ const rawData = result.data;
197
+ if (existingProfile) {
198
+ const existingData = JSON.parse(existingProfile.profileData);
199
+ return userProfileManager.mergeProfileData(existingData, rawData);
162
200
  }
163
- return result.data.memories || [];
201
+ return rawData;
164
202
  }
@@ -0,0 +1,2 @@
1
+ export declare function getUserProfileContext(userId: string): string | null;
2
+ //# sourceMappingURL=profile-context.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"profile-context.d.ts","sourceRoot":"","sources":["../../../src/services/user-profile/profile-context.ts"],"names":[],"mappings":"AAGA,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CA6CnE"}