om-memory-system 3.0.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 (221) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +587 -0
  3. package/dist/adapters/opencode/auto-capture-host.d.ts +3 -0
  4. package/dist/adapters/opencode/auto-capture-host.js +29 -0
  5. package/dist/adapters/opencode/auto-capture-summary.d.ts +4 -0
  6. package/dist/adapters/opencode/auto-capture-summary.js +147 -0
  7. package/dist/adapters/opencode/conversation.d.ts +2 -0
  8. package/dist/adapters/opencode/conversation.js +50 -0
  9. package/dist/adapters/opencode/user-prompt.d.ts +9 -0
  10. package/dist/adapters/opencode/user-prompt.js +26 -0
  11. package/dist/adapters/pi/capture.d.ts +41 -0
  12. package/dist/adapters/pi/capture.js +91 -0
  13. package/dist/adapters/pi/conversation.d.ts +38 -0
  14. package/dist/adapters/pi/conversation.js +118 -0
  15. package/dist/adapters/pi/extension.d.ts +18 -0
  16. package/dist/adapters/pi/extension.js +234 -0
  17. package/dist/adapters/pi/import-command.d.ts +29 -0
  18. package/dist/adapters/pi/import-command.js +232 -0
  19. package/dist/adapters/pi/profile.d.ts +39 -0
  20. package/dist/adapters/pi/profile.js +148 -0
  21. package/dist/adapters/pi/provider.d.ts +50 -0
  22. package/dist/adapters/pi/provider.js +77 -0
  23. package/dist/config.d.ts +290 -0
  24. package/dist/config.js +742 -0
  25. package/dist/core/capture-context.d.ts +4 -0
  26. package/dist/core/capture-context.js +132 -0
  27. package/dist/core/capture.d.ts +15 -0
  28. package/dist/core/capture.js +68 -0
  29. package/dist/core/extraction.d.ts +80 -0
  30. package/dist/core/extraction.js +140 -0
  31. package/dist/core/host.d.ts +52 -0
  32. package/dist/core/host.js +1 -0
  33. package/dist/core/memory-operations.d.ts +27 -0
  34. package/dist/core/memory-operations.js +247 -0
  35. package/dist/core/retrieval.d.ts +12 -0
  36. package/dist/core/retrieval.js +72 -0
  37. package/dist/importer/discovery.d.ts +31 -0
  38. package/dist/importer/discovery.js +88 -0
  39. package/dist/importer/importer.d.ts +87 -0
  40. package/dist/importer/importer.js +260 -0
  41. package/dist/importer/ledger.d.ts +44 -0
  42. package/dist/importer/ledger.js +137 -0
  43. package/dist/importer/session-loader.d.ts +19 -0
  44. package/dist/importer/session-loader.js +12 -0
  45. package/dist/index.d.ts +24 -0
  46. package/dist/index.js +601 -0
  47. package/dist/plugin.d.ts +9 -0
  48. package/dist/plugin.js +11 -0
  49. package/dist/services/ai/ai-provider-factory.d.ts +7 -0
  50. package/dist/services/ai/ai-provider-factory.js +41 -0
  51. package/dist/services/ai/api-key-placeholder.d.ts +1 -0
  52. package/dist/services/ai/api-key-placeholder.js +14 -0
  53. package/dist/services/ai/internal-capture-sessions.d.ts +6 -0
  54. package/dist/services/ai/internal-capture-sessions.js +33 -0
  55. package/dist/services/ai/opencode-diagnostics.d.ts +7 -0
  56. package/dist/services/ai/opencode-diagnostics.js +36 -0
  57. package/dist/services/ai/opencode-host-config.d.ts +10 -0
  58. package/dist/services/ai/opencode-host-config.js +51 -0
  59. package/dist/services/ai/opencode-provider-loader.d.ts +3 -0
  60. package/dist/services/ai/opencode-provider-loader.js +4 -0
  61. package/dist/services/ai/opencode-provider.d.ts +92 -0
  62. package/dist/services/ai/opencode-provider.js +419 -0
  63. package/dist/services/ai/opencode-sdk-client.d.ts +6 -0
  64. package/dist/services/ai/opencode-sdk-client.js +38 -0
  65. package/dist/services/ai/profile-llm-client.d.ts +2 -0
  66. package/dist/services/ai/profile-llm-client.js +27 -0
  67. package/dist/services/ai/provider-config.d.ts +17 -0
  68. package/dist/services/ai/provider-config.js +30 -0
  69. package/dist/services/ai/providers/anthropic-messages.d.ts +34 -0
  70. package/dist/services/ai/providers/anthropic-messages.js +220 -0
  71. package/dist/services/ai/providers/base-provider.d.ts +24 -0
  72. package/dist/services/ai/providers/base-provider.js +24 -0
  73. package/dist/services/ai/providers/google-gemini.d.ts +15 -0
  74. package/dist/services/ai/providers/google-gemini.js +228 -0
  75. package/dist/services/ai/providers/minimax.d.ts +35 -0
  76. package/dist/services/ai/providers/minimax.js +55 -0
  77. package/dist/services/ai/providers/openai-chat-completion.d.ts +22 -0
  78. package/dist/services/ai/providers/openai-chat-completion.js +387 -0
  79. package/dist/services/ai/providers/openai-responses.d.ts +13 -0
  80. package/dist/services/ai/providers/openai-responses.js +182 -0
  81. package/dist/services/ai/providers/orcarouter.d.ts +51 -0
  82. package/dist/services/ai/providers/orcarouter.js +66 -0
  83. package/dist/services/ai/session/ai-session-manager.d.ts +24 -0
  84. package/dist/services/ai/session/ai-session-manager.js +211 -0
  85. package/dist/services/ai/session/session-types.d.ts +42 -0
  86. package/dist/services/ai/session/session-types.js +1 -0
  87. package/dist/services/ai/tools/tool-schema.d.ts +40 -0
  88. package/dist/services/ai/tools/tool-schema.js +24 -0
  89. package/dist/services/ai/validators/user-profile-validator.d.ts +12 -0
  90. package/dist/services/ai/validators/user-profile-validator.js +114 -0
  91. package/dist/services/api-handlers.d.ts +186 -0
  92. package/dist/services/api-handlers.js +1284 -0
  93. package/dist/services/auth-token.d.ts +14 -0
  94. package/dist/services/auth-token.js +59 -0
  95. package/dist/services/auto-capture.d.ts +3 -0
  96. package/dist/services/auto-capture.js +127 -0
  97. package/dist/services/cleanup-service.d.ts +22 -0
  98. package/dist/services/cleanup-service.js +109 -0
  99. package/dist/services/client.d.ts +134 -0
  100. package/dist/services/client.js +327 -0
  101. package/dist/services/context.d.ts +10 -0
  102. package/dist/services/context.js +27 -0
  103. package/dist/services/cors.d.ts +12 -0
  104. package/dist/services/cors.js +52 -0
  105. package/dist/services/deduplication-service.d.ts +28 -0
  106. package/dist/services/deduplication-service.js +111 -0
  107. package/dist/services/embedding.d.ts +36 -0
  108. package/dist/services/embedding.js +200 -0
  109. package/dist/services/jsonc.d.ts +6 -0
  110. package/dist/services/jsonc.js +76 -0
  111. package/dist/services/language-detector.d.ts +2 -0
  112. package/dist/services/language-detector.js +33 -0
  113. package/dist/services/legacy-migration.d.ts +77 -0
  114. package/dist/services/legacy-migration.js +343 -0
  115. package/dist/services/logger.d.ts +1 -0
  116. package/dist/services/logger.js +119 -0
  117. package/dist/services/memory-portability-service.d.ts +37 -0
  118. package/dist/services/memory-portability-service.js +340 -0
  119. package/dist/services/memory-scope.d.ts +16 -0
  120. package/dist/services/memory-scope.js +43 -0
  121. package/dist/services/migration-service.d.ts +42 -0
  122. package/dist/services/migration-service.js +307 -0
  123. package/dist/services/onnxruntime-resolve.d.ts +24 -0
  124. package/dist/services/onnxruntime-resolve.js +174 -0
  125. package/dist/services/privacy.d.ts +26 -0
  126. package/dist/services/privacy.js +69 -0
  127. package/dist/services/request-timeouts.d.ts +9 -0
  128. package/dist/services/request-timeouts.js +11 -0
  129. package/dist/services/runtime-require.d.ts +17 -0
  130. package/dist/services/runtime-require.js +75 -0
  131. package/dist/services/secret-resolver.d.ts +1 -0
  132. package/dist/services/secret-resolver.js +65 -0
  133. package/dist/services/shard-inventory-service.d.ts +69 -0
  134. package/dist/services/shard-inventory-service.js +315 -0
  135. package/dist/services/shard-path-migration-service.d.ts +31 -0
  136. package/dist/services/shard-path-migration-service.js +367 -0
  137. package/dist/services/tag-prefix-migration.d.ts +60 -0
  138. package/dist/services/tag-prefix-migration.js +439 -0
  139. package/dist/services/tag-prefix-scan.d.ts +26 -0
  140. package/dist/services/tag-prefix-scan.js +91 -0
  141. package/dist/services/tags.d.ts +29 -0
  142. package/dist/services/tags.js +251 -0
  143. package/dist/services/turso/connection-manager.d.ts +15 -0
  144. package/dist/services/turso/connection-manager.js +163 -0
  145. package/dist/services/turso/cross-process-write-lock.d.ts +1 -0
  146. package/dist/services/turso/cross-process-write-lock.js +89 -0
  147. package/dist/services/turso/legacy-migrator.d.ts +1 -0
  148. package/dist/services/turso/legacy-migrator.js +540 -0
  149. package/dist/services/turso/lifecycle.d.ts +1 -0
  150. package/dist/services/turso/lifecycle.js +14 -0
  151. package/dist/services/turso/operation-lock.d.ts +2 -0
  152. package/dist/services/turso/operation-lock.js +61 -0
  153. package/dist/services/turso/ready.d.ts +2 -0
  154. package/dist/services/turso/ready.js +36 -0
  155. package/dist/services/turso/shard-manager.d.ts +38 -0
  156. package/dist/services/turso/shard-manager.js +496 -0
  157. package/dist/services/turso/sqlite-handle-release.d.ts +16 -0
  158. package/dist/services/turso/sqlite-handle-release.js +56 -0
  159. package/dist/services/turso/turso-db.d.ts +18 -0
  160. package/dist/services/turso/turso-db.js +53 -0
  161. package/dist/services/turso/types.d.ts +42 -0
  162. package/dist/services/turso/types.js +1 -0
  163. package/dist/services/turso/vector-search.d.ts +41 -0
  164. package/dist/services/turso/vector-search.js +384 -0
  165. package/dist/services/turso/vector-utils.d.ts +7 -0
  166. package/dist/services/turso/vector-utils.js +67 -0
  167. package/dist/services/user-memory-learning.d.ts +97 -0
  168. package/dist/services/user-memory-learning.js +673 -0
  169. package/dist/services/user-profile/ai-cleanup.d.ts +58 -0
  170. package/dist/services/user-profile/ai-cleanup.js +486 -0
  171. package/dist/services/user-profile/profile-context.d.ts +1 -0
  172. package/dist/services/user-profile/profile-context.js +105 -0
  173. package/dist/services/user-profile/profile-utils.d.ts +2 -0
  174. package/dist/services/user-profile/profile-utils.js +45 -0
  175. package/dist/services/user-profile/types.d.ts +92 -0
  176. package/dist/services/user-profile/types.js +1 -0
  177. package/dist/services/user-profile/user-profile-manager.d.ts +65 -0
  178. package/dist/services/user-profile/user-profile-manager.js +1660 -0
  179. package/dist/services/user-prompt/user-prompt-manager.d.ts +53 -0
  180. package/dist/services/user-prompt/user-prompt-manager.js +303 -0
  181. package/dist/services/web-api-auth.d.ts +3 -0
  182. package/dist/services/web-api-auth.js +25 -0
  183. package/dist/services/web-auth.d.ts +46 -0
  184. package/dist/services/web-auth.js +105 -0
  185. package/dist/services/web-server.d.ts +57 -0
  186. package/dist/services/web-server.js +659 -0
  187. package/dist/shared/api/index.d.ts +4 -0
  188. package/dist/shared/api/index.js +2 -0
  189. package/dist/shared/api/portability-schemas.d.ts +56 -0
  190. package/dist/shared/api/portability-schemas.js +39 -0
  191. package/dist/shared/api/schemas.d.ts +295 -0
  192. package/dist/shared/api/schemas.js +134 -0
  193. package/dist/shared/api/types.d.ts +28 -0
  194. package/dist/shared/api/types.js +1 -0
  195. package/dist/types/index.d.ts +27 -0
  196. package/dist/types/index.js +1 -0
  197. package/dist/utils/context-limit.d.ts +10 -0
  198. package/dist/utils/context-limit.js +43 -0
  199. package/dist/utils/math.d.ts +7 -0
  200. package/dist/utils/math.js +48 -0
  201. package/dist/utils/profile.d.ts +2 -0
  202. package/dist/utils/profile.js +15 -0
  203. package/dist/v2/adapter.d.ts +5 -0
  204. package/dist/v2/adapter.js +218 -0
  205. package/dist/v2/legacy-client.d.ts +15 -0
  206. package/dist/v2/legacy-client.js +257 -0
  207. package/dist/v2/memory-bridge.d.ts +18 -0
  208. package/dist/v2/memory-bridge.js +29 -0
  209. package/dist/v2/plugin.d.ts +3 -0
  210. package/dist/v2/plugin.js +24 -0
  211. package/dist/web/assets/index-DPjEpD1h.css +2 -0
  212. package/dist/web/assets/index-do26c4JJ.js +122 -0
  213. package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
  214. package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
  215. package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
  216. package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
  217. package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
  218. package/dist/web/favicon.ico +0 -0
  219. package/dist/web/index.html +14 -0
  220. package/dist/web/omms-icon.png +0 -0
  221. package/package.json +138 -0
@@ -0,0 +1,673 @@
1
+ import { getTags } from "./tags.js";
2
+ import { log } from "./logger.js";
3
+ import { CONFIG } from "../config.js";
4
+ import { userPromptManager } from "./user-prompt/user-prompt-manager.js";
5
+ import { userProfileManager } from "./user-profile/user-profile-manager.js";
6
+ import { sortProfileItems } from "../utils/profile.js";
7
+ import { loadOpencodeProvider } from "./ai/opencode-provider-loader.js";
8
+ let isLearningRunning = false;
9
+ export function shouldRunAutomaticProfileCleanup(previousPromptCount, addedPromptCount, interval = CONFIG.userProfileAutoCleanupInterval) {
10
+ if (!CONFIG.userProfileAutoCleanupEnabled || !Number.isInteger(interval) || interval <= 0) {
11
+ return false;
12
+ }
13
+ return (Math.floor(previousPromptCount / interval) <
14
+ Math.floor((previousPromptCount + addedPromptCount) / interval));
15
+ }
16
+ async function runAutomaticProfileCleanup(userId) {
17
+ try {
18
+ const profile = await userProfileManager.getActiveProfile(userId);
19
+ if (!profile)
20
+ return;
21
+ const profileData = JSON.parse(profile.profileData);
22
+ const itemCount = profileData.preferences.length + profileData.patterns.length + profileData.workflows.length;
23
+ if (itemCount < 2)
24
+ return;
25
+ const { aiCleanupProfile } = await import("./user-profile/ai-cleanup.js");
26
+ const result = await aiCleanupProfile(profileData);
27
+ if (result.diff.merged.length === 0 && result.diff.removed.length === 0)
28
+ return;
29
+ const updated = await userProfileManager.updateProfile(profile.id, result.cleaned, 0, `Automatic AI cleanup: ${result.diff.merged.length} merged, ${result.diff.removed.length} removed`);
30
+ log("user-profile-learning: automatic cleanup complete", {
31
+ userId,
32
+ updated,
33
+ merged: result.diff.merged.length,
34
+ removed: result.diff.removed.length,
35
+ });
36
+ }
37
+ catch (error) {
38
+ log("user-profile-learning: automatic cleanup failed", { userId, error: String(error) });
39
+ }
40
+ }
41
+ export async function performUserProfileLearning(ctx, directory) {
42
+ if (isLearningRunning)
43
+ return;
44
+ if (!CONFIG.autoCaptureProviderStatus || !CONFIG.autoCaptureProviderStatus.ready) {
45
+ log("user-profile-learning: skipped (provider not ready)", {
46
+ issues: CONFIG.autoCaptureProviderStatus?.issues ?? ["status undefined"],
47
+ });
48
+ return;
49
+ }
50
+ isLearningRunning = true;
51
+ try {
52
+ const count = await userPromptManager.countUnanalyzedForUserLearning();
53
+ const threshold = CONFIG.userProfileAnalysisInterval;
54
+ log("user-profile-learning: check", { count, threshold });
55
+ if (count < threshold) {
56
+ return;
57
+ }
58
+ const prompts = await userPromptManager.getPromptsForUserLearning(threshold);
59
+ if (prompts.length === 0) {
60
+ return;
61
+ }
62
+ const tags = getTags(directory);
63
+ const userId = tags.user.userEmail || "unknown";
64
+ let existingProfile = await userProfileManager.getActiveProfile(userId);
65
+ const analysisStartTime = Date.now();
66
+ let validationPrompt;
67
+ let validationPrefKeys;
68
+ if (existingProfile && CONFIG.userProfileValidationEnabled) {
69
+ const profileData = JSON.parse(existingProfile.profileData);
70
+ const { data: decayed } = userProfileManager.decayInMemory(profileData);
71
+ for (const arr of [decayed.preferences, decayed.patterns, decayed.workflows]) {
72
+ for (const item of arr) {
73
+ if (item.pendingValidation && (item.lastSeen || 0) < analysisStartTime) {
74
+ item.pendingValidation = false;
75
+ item.alpha = (item.alpha || 1) + 1;
76
+ userProfileManager.syncConfidence(item);
77
+ }
78
+ }
79
+ }
80
+ existingProfile = { ...existingProfile, profileData: JSON.stringify(decayed) };
81
+ const topPrefs = sortProfileItems(decayed.preferences, "confidence").slice(0, 5);
82
+ const topPats = sortProfileItems(decayed.patterns, "frequency").slice(0, 3);
83
+ const hasValidator = topPrefs.length >= 5;
84
+ if (hasValidator) {
85
+ const allValidated = [...topPrefs, ...topPats];
86
+ validationPrefKeys = allValidated.map((p) => `${p.category || "_"}||${(p.description || "").substring(0, 30)}`);
87
+ log("user-profile-learning: validation enabled", {
88
+ topPrefs: topPrefs.map((p) => `[${p.category}] ${(p.description || "").substring(0, 30)}`),
89
+ topPats: topPats.map((p) => `[${p.category}] ${(p.description || "").substring(0, 30)}`),
90
+ });
91
+ validationPrompt = `## Task 3: Validate Existing Profile Entries
92
+
93
+ CRITICAL: Complete Tasks 1-2 (new observations) FIRST. This task is separate — only check whether the entries below still match recent behavior. Do NOT let these descriptions influence your new observations.
94
+
95
+ ${allValidated.map((p, i) => `${i}. [${p.category || "_"}] ${(p.description || "").substring(0, 30)} (conf: ${Math.round((p.confidence || 0) * 100) / 100})`).join("\n")}
96
+
97
+ For each entry above, judge whether recent prompts confirm or contradict it. Output:
98
+ {"validations": [{"index": 0, "verdict": "confirmed|contradicted|no_evidence|inaccurate|oversimplified", "reason": "one sentence"}]}
99
+
100
+ Rules:
101
+ - confirmed: recent prompts show clear evidence
102
+ - contradicted: recent prompts show the user has changed
103
+ - inaccurate: the description is directionally wrong (opposite behavior seen)
104
+ - oversimplified: the description is too vague, missing important nuance
105
+ - no_evidence: recent prompts don't address this topic
106
+ - Only mark contradicted if there is explicit evidence the user's behavior has changed`;
107
+ }
108
+ else {
109
+ log("user-profile-learning: validation skipped", {
110
+ prefCount: topPrefs.length,
111
+ reason: "needs ≥ 5 preferences",
112
+ });
113
+ }
114
+ }
115
+ else if (existingProfile) {
116
+ const profileData = JSON.parse(existingProfile.profileData);
117
+ const { data: decayed } = userProfileManager.decayInMemory(profileData);
118
+ for (const arr of [decayed.preferences, decayed.patterns, decayed.workflows]) {
119
+ for (const item of arr) {
120
+ if (item.pendingValidation && (item.lastSeen || 0) < analysisStartTime) {
121
+ item.pendingValidation = false;
122
+ item.alpha = (item.alpha || 1) + 1;
123
+ userProfileManager.syncConfidence(item);
124
+ }
125
+ }
126
+ }
127
+ existingProfile = { ...existingProfile, profileData: JSON.stringify(decayed) };
128
+ }
129
+ const context = buildUserAnalysisContext(prompts, existingProfile, validationPrompt);
130
+ const analysisResult = await analyzeUserProfile(context, existingProfile);
131
+ log("user-profile-learning: analyze done", { hasResult: !!analysisResult });
132
+ if (!analysisResult) {
133
+ await userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
134
+ if (prompts.length >= 10 && existingProfile) {
135
+ buildLearningPaths(prompts, existingProfile.id).catch(() => { });
136
+ }
137
+ return;
138
+ }
139
+ const { raw: llmResult, merged: initialMerged } = analysisResult;
140
+ let cleanupPreviousPromptCount = 0;
141
+ if (existingProfile) {
142
+ let updatedProfileData = initialMerged;
143
+ const MAX_RETRIES = 2;
144
+ let retries = 0;
145
+ let success = false;
146
+ while (!success && retries <= MAX_RETRIES) {
147
+ if (retries > 0) {
148
+ existingProfile = await userProfileManager.getActiveProfile(userId);
149
+ if (!existingProfile)
150
+ break;
151
+ const retryProfileData = JSON.parse(existingProfile.profileData);
152
+ const { data: decayedRetry } = userProfileManager.decayInMemory(retryProfileData);
153
+ existingProfile = { ...existingProfile, profileData: JSON.stringify(decayedRetry) };
154
+ updatedProfileData = await userProfileManager.mergeProfileData(decayedRetry, llmResult, undefined, existingProfile.id);
155
+ log("user-profile-learning: retry merge", {
156
+ retry: retries,
157
+ profileId: existingProfile.id,
158
+ });
159
+ }
160
+ cleanupPreviousPromptCount = existingProfile.totalPromptsAnalyzed;
161
+ let changeSummary = generateChangeSummary(JSON.parse(existingProfile.profileData), updatedProfileData);
162
+ const validationSummary = await applyValidations(updatedProfileData, llmResult, existingProfile.id, validationPrefKeys);
163
+ if (validationSummary) {
164
+ changeSummary = changeSummary + "; " + validationSummary;
165
+ }
166
+ success = await userProfileManager.updateProfile(existingProfile.id, updatedProfileData, prompts.length, changeSummary);
167
+ if (!success) {
168
+ log("User profile update conflict, retrying", {
169
+ profileId: existingProfile.id,
170
+ userId,
171
+ retry: retries,
172
+ });
173
+ }
174
+ retries++;
175
+ }
176
+ if (!success) {
177
+ // Mark the batch anyway so the same prompts are not retried forever
178
+ // (token burn + repeated toasts on every subsequent idle).
179
+ log("User profile update conflict: exhausted retries", {
180
+ profileId: existingProfile?.id,
181
+ userId,
182
+ });
183
+ await userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
184
+ return;
185
+ }
186
+ await userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
187
+ }
188
+ else {
189
+ await userProfileManager.createProfile(userId, tags.user.displayName || "Unknown", tags.user.userName || "unknown", tags.user.userEmail || "unknown", llmResult, prompts.length);
190
+ await userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
191
+ }
192
+ if (shouldRunAutomaticProfileCleanup(cleanupPreviousPromptCount, prompts.length)) {
193
+ await runAutomaticProfileCleanup(userId);
194
+ }
195
+ if (CONFIG.showUserProfileToasts) {
196
+ await ctx.client?.tui
197
+ .showToast({
198
+ body: {
199
+ title: "User Profile Updated",
200
+ message: `Analyzed ${prompts.length} of ${count} pending prompts and updated your profile`,
201
+ variant: "success",
202
+ duration: 3000,
203
+ },
204
+ })
205
+ .catch(() => { });
206
+ }
207
+ }
208
+ catch (error) {
209
+ // Guard against corrupt stored profileData (JSON.parse throws) and any other
210
+ // fault: this runs fire-and-forget from the idle timer, so an uncaught rejection
211
+ // would surface as an unhandled promise rejection. The caller (src/index.ts idle
212
+ // timer) already wraps this call in its own try/catch, and issue #265 requires
213
+ // provider errors to propagate instead of being masked, so rethrow after logging.
214
+ log("user-profile-learning: aborted", { error: String(error) });
215
+ throw error;
216
+ }
217
+ finally {
218
+ isLearningRunning = false;
219
+ }
220
+ }
221
+ function generateChangeSummary(oldProfile, newProfile) {
222
+ const changes = [];
223
+ const prefDiff = newProfile.preferences.length - oldProfile.preferences.length;
224
+ if (prefDiff > 0)
225
+ changes.push(`+${prefDiff} preferences`);
226
+ const patternDiff = newProfile.patterns.length - oldProfile.patterns.length;
227
+ if (patternDiff > 0)
228
+ changes.push(`+${patternDiff} patterns`);
229
+ const workflowDiff = newProfile.workflows.length - oldProfile.workflows.length;
230
+ if (workflowDiff > 0)
231
+ changes.push(`+${workflowDiff} workflows`);
232
+ return changes.length > 0 ? changes.join(", ") : "Profile refinement";
233
+ }
234
+ function buildCategorySummary(profileData) {
235
+ const prefCats = [...new Set(profileData.preferences.map((p) => p.category))];
236
+ const patCats = [...new Set(profileData.patterns.map((p) => p.category))];
237
+ const catParts = [];
238
+ if (prefCats.length > 0) {
239
+ const catCounts = prefCats
240
+ .map((cat) => {
241
+ const cnt = profileData.preferences.filter((p) => p.category === cat).length;
242
+ return `${cat} (${cnt})`;
243
+ })
244
+ .join(", ");
245
+ catParts.push(`Preference categories: ${catCounts}`);
246
+ }
247
+ if (patCats.length > 0) {
248
+ const catCounts = patCats
249
+ .map((cat) => {
250
+ const cnt = profileData.patterns.filter((p) => p.category === cat).length;
251
+ return `${cat} (${cnt})`;
252
+ })
253
+ .join(", ");
254
+ catParts.push(`Pattern categories: ${catCounts}`);
255
+ }
256
+ const catSection = catParts.length > 0
257
+ ? `## Existing Categories\nUse these exact category names when your observation fits:\n\n${catParts.join("\n")}\n`
258
+ : "";
259
+ const prefCount = profileData.preferences.length;
260
+ const patCount = profileData.patterns.length;
261
+ const wfCount = profileData.workflows.length;
262
+ const wfParts = [];
263
+ if (wfCount > 0) {
264
+ wfParts.push("## Existing Workflows\nFor reference — only report a workflow when recent prompts show a genuinely NEW sequence, NOT a minor variant of an existing one:");
265
+ profileData.workflows.forEach((wf, i) => {
266
+ const steps = wf.steps?.length
267
+ ? ` (freq ${wf.frequency || 1}x: ${wf.steps.join(" → ")})`
268
+ : "";
269
+ wfParts.push(`${i + 1}. ${wf.description}${steps}`);
270
+ });
271
+ }
272
+ const countSection = `## Profile Size\nPreferences: ${prefCount} | Patterns: ${patCount} | Workflows: ${wfCount}\n
273
+ New observations are matched via embedding cosine similarity — write descriptions in your own words; do not reuse existing wording.`;
274
+ return [catSection, ...wfParts, countSection].filter(Boolean).join("\n");
275
+ }
276
+ function buildUserAnalysisContext(prompts, existingProfile, validationPrompt) {
277
+ const base = `# User Profile Analysis
278
+
279
+ Analyze ${prompts.length} user prompts to ${existingProfile ? "update" : "create"} the user profile.
280
+ ${existingProfile ? `The merge system will automatically connect your observations to existing profile entries — you only need to describe what you see in these recent prompts.` : `Create a new user profile from scratch based on the prompts below.`}
281
+
282
+ ${existingProfile ? buildCategorySummary(JSON.parse(existingProfile.profileData)) : ""}
283
+ ## Recent Prompts
284
+
285
+ ${prompts.map((p, i) => `${i + 1}. ${p.content}`).join("\n\n")}
286
+
287
+ ## Analysis Guidelines
288
+
289
+ Identify and ${existingProfile ? "report" : "create"}:
290
+
291
+ 1. **Preferences**
292
+ - Code style, communication style, tool preferences
293
+ - Assign confidence 0.3-0.5 based on evidence strength in these recent prompts
294
+ - Include 1-3 example prompts as evidence
295
+ - **Revealed preferences**: when the user chooses one approach over alternatives (e.g. picks simpler solution, skips certain steps), capture the choice as a lower-confidence preference (0.3-0.5). What the user does NOT do is also a signal.
296
+
297
+ 2. **Patterns**
298
+ - Recurring topics, problem domains, technical interests seen in these prompts
299
+ - Track frequency of occurrence
300
+
301
+ 3. **Workflows**
302
+ - Distinct, named step sequences the user follows repeatedly
303
+ - Each workflow should represent a DIFFERENT activity (different purpose, different steps)
304
+ - Break down into 3-6 concrete, observable steps, NOT abstract phases
305
+ - Do NOT repeat the same workflow every cycle — only output when you observe a NEW recurring sequence
306
+ - Examples of distinct workflows: "debugging workflow", "code review workflow", "learning workflow", "refactoring workflow"
307
+
308
+ CRITICAL: Only output observations grounded in the RECENT PROMPTS above. Write descriptions in your own words — the system matches by embedding similarity, not exact wording. Do NOT output entries that lack evidence in recent prompts. Put the core semantics at the beginning of each description, keeping descriptions concise and specific (under 120 characters). Do NOT extract one-time debugging tasks, environment setup issues, or specific error investigations as preferences — these are transient events, not behavioral patterns.
309
+
310
+ ## Few-Shot Examples
311
+
312
+ ❌ Do NOT extract as preference:
313
+ - "User is debugging a NullPointerException in auth service" (one-time debugging task)
314
+ - "User installed Redis for the first time" (one-time setup event)
315
+ - "User ran npm audit fix" (routine maintenance, not a behavioral pattern)
316
+
317
+ ✅ DO extract as preference:
318
+ - "User prefers functional programming style over OOP"
319
+ - "User consistently writes tests before implementation"
320
+ - "User asks for explanations before accepting code changes"
321
+
322
+ ✅ DO extract as workflow (distinct, non-overlapping):
323
+ - Debugging workflow: "reproduce the error → check logs → grep source code → trace call chain → propose fix → verify fix"
324
+ - Code review workflow: "read the diff → check edge cases → verify consistency with existing patterns → report issues → suggest alternatives"
325
+ - Learning workflow: "ask for explanation → request examples → test understanding with a small task → apply to real problem"
326
+
327
+ ❌ Do NOT extract as workflow:
328
+ - "User analyzes problems and verifies solutions" (too abstract — not a concrete step sequence)
329
+ - "User writes code and tests it" (too generic — covers everything)`;
330
+ const maxBytes = CONFIG.userProfileMaxContextBytes ?? 32768;
331
+ const truncate = (s) => s.length > maxBytes
332
+ ? s.substring(0, maxBytes) + "\n[... context truncated to userProfileMaxContextBytes ...]"
333
+ : s;
334
+ if (validationPrompt) {
335
+ return truncate(base + "\n\n" + validationPrompt);
336
+ }
337
+ return truncate(base);
338
+ }
339
+ import { createUserProfileAnalysisSchema, USER_PROFILE_LLM_CONFIDENCE_MAX, } from "../core/extraction.js";
340
+ export { createUserProfileAnalysisSchema, USER_PROFILE_LLM_CONFIDENCE_MAX };
341
+ export function createUserProfileToolSchema(existingProfile) {
342
+ return {
343
+ type: "function",
344
+ function: {
345
+ name: "update_user_profile",
346
+ description: existingProfile
347
+ ? "Update existing user profile with new insights"
348
+ : "Create new user profile",
349
+ parameters: {
350
+ type: "object",
351
+ properties: {
352
+ preferences: {
353
+ type: "array",
354
+ items: {
355
+ type: "object",
356
+ properties: {
357
+ category: { type: "string" },
358
+ description: { type: "string" },
359
+ confidence: {
360
+ type: "number",
361
+ minimum: 0,
362
+ maximum: USER_PROFILE_LLM_CONFIDENCE_MAX,
363
+ },
364
+ evidence: { type: "array", items: { type: "string" }, maxItems: 3 },
365
+ },
366
+ required: ["category", "description", "confidence", "evidence"],
367
+ },
368
+ },
369
+ patterns: {
370
+ type: "array",
371
+ items: {
372
+ type: "object",
373
+ properties: {
374
+ category: { type: "string" },
375
+ description: { type: "string" },
376
+ },
377
+ required: ["category", "description"],
378
+ },
379
+ },
380
+ workflows: {
381
+ type: "array",
382
+ items: {
383
+ type: "object",
384
+ properties: {
385
+ description: { type: "string" },
386
+ steps: { type: "array", items: { type: "string" } },
387
+ },
388
+ required: ["description", "steps"],
389
+ },
390
+ },
391
+ validations: {
392
+ type: "array",
393
+ items: {
394
+ type: "object",
395
+ properties: {
396
+ index: { type: "number" },
397
+ verdict: {
398
+ type: "string",
399
+ enum: [
400
+ "confirmed",
401
+ "contradicted",
402
+ "no_evidence",
403
+ "inaccurate",
404
+ "oversimplified",
405
+ ],
406
+ },
407
+ reason: { type: "string" },
408
+ },
409
+ required: ["index", "verdict", "reason"],
410
+ },
411
+ },
412
+ },
413
+ required: ["preferences", "patterns", "workflows"],
414
+ },
415
+ },
416
+ };
417
+ }
418
+ async function applyValidations(profileData, llmResult, profileId, prefKeys) {
419
+ const validations = llmResult.validations;
420
+ if (!validations?.length || !prefKeys?.length)
421
+ return null;
422
+ const allItems = [...profileData.preferences, ...profileData.patterns];
423
+ const results = [];
424
+ let confirmed = 0;
425
+ let contradicted = 0;
426
+ let inaccurate = 0;
427
+ let oversimplified = 0;
428
+ for (const v of validations) {
429
+ const key = prefKeys[v.index];
430
+ if (!key)
431
+ continue;
432
+ const item = allItems.find((i) => `${i.category || "_"}||${(i.description || "").substring(0, 30)}` === key);
433
+ if (!item) {
434
+ log("user-profile-learning: validation match failed", { index: v.index, key });
435
+ continue;
436
+ }
437
+ if (v.verdict === "confirmed") {
438
+ item.alpha = (item.alpha || 1) + 0.5;
439
+ userProfileManager.syncConfidence(item);
440
+ confirmed++;
441
+ results.push(`confirmed [${v.index}] ${v.reason}`);
442
+ }
443
+ else if (v.verdict === "contradicted") {
444
+ const oldAlpha = item.alpha || 1;
445
+ item.alpha = oldAlpha * 0.75;
446
+ item.beta = (item.beta || 1) + oldAlpha * 0.25;
447
+ userProfileManager.syncConfidence(item);
448
+ contradicted++;
449
+ results.push(`contradicted [${v.index}] ${v.reason}`);
450
+ }
451
+ else if (v.verdict === "inaccurate") {
452
+ const oldAlpha = item.alpha || 1;
453
+ item.alpha = oldAlpha * 0.6;
454
+ item.beta = (item.beta || 1) + oldAlpha * 0.4;
455
+ userProfileManager.syncConfidence(item);
456
+ inaccurate++;
457
+ results.push(`inaccurate [${v.index}] ${v.reason}`);
458
+ }
459
+ else if (v.verdict === "oversimplified") {
460
+ const oldAlpha = item.alpha || 1;
461
+ item.alpha = oldAlpha * 0.85;
462
+ item.beta = (item.beta || 1) + oldAlpha * 0.15;
463
+ userProfileManager.syncConfidence(item);
464
+ oversimplified++;
465
+ results.push(`oversimplified [${v.index}] ${v.reason}`);
466
+ const evidence = item.evidence;
467
+ if (Array.isArray(evidence) && evidence.length >= 3) {
468
+ const itemType = profileData.preferences.includes(item) ? "preference" : "pattern";
469
+ // Await so the in-place description/centroid mutation completes before the
470
+ // caller serializes updatedProfileData — otherwise the evolved description is
471
+ // included or lost nondeterministically. Failures stay non-fatal.
472
+ try {
473
+ await userProfileManager.evolveAndUpdate(item, itemType, profileId);
474
+ }
475
+ catch {
476
+ // description evolution is best-effort
477
+ }
478
+ }
479
+ }
480
+ else {
481
+ results.push(`no_evidence [${v.index}] ${v.reason}`);
482
+ }
483
+ }
484
+ if (results.length > 0) {
485
+ log("user-profile-learning: validation results", { validated: results });
486
+ }
487
+ if (confirmed === 0 && contradicted === 0 && inaccurate === 0 && oversimplified === 0)
488
+ return null;
489
+ return `validated: ${confirmed} confirmed, ${contradicted} contradicted, ${inaccurate} inaccurate, ${oversimplified} oversimplified`;
490
+ }
491
+ async function analyzeUserProfile(context, existingProfile) {
492
+ log("user-profile-learning: analyze called", { hasProfile: !!existingProfile });
493
+ let opencodeProviderError;
494
+ if (CONFIG.opencodeProvider && CONFIG.opencodeModel) {
495
+ log("user-profile-learning: trying opencode provider");
496
+ try {
497
+ const { generateStructuredOutput } = await loadOpencodeProvider();
498
+ const { getOpenCodeClient } = await import("./ai/profile-llm-client.js");
499
+ log("user-profile-learning: opencode provider diag", {
500
+ provider: CONFIG.opencodeProvider,
501
+ model: CONFIG.opencodeModel,
502
+ });
503
+ const v2Client = await getOpenCodeClient();
504
+ const systemPrompt = `You are a user behavior analyst for a coding assistant.
505
+
506
+ Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile.
507
+
508
+ CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts.
509
+
510
+ CRITICAL: All JSON string values MUST escape double quotes with backslash. Do NOT use unescaped quotation marks inside string values.
511
+
512
+ Use the update_user_profile tool to save the ${existingProfile ? "updated" : "new"} profile.`;
513
+ const { z } = await import("zod");
514
+ const schema = createUserProfileAnalysisSchema(z);
515
+ log("user-profile-learning: calling LLM", { contextLen: context.length });
516
+ const result = await Promise.race([
517
+ generateStructuredOutput({
518
+ client: v2Client,
519
+ providerID: CONFIG.opencodeProvider,
520
+ modelID: CONFIG.opencodeModel,
521
+ systemPrompt,
522
+ userPrompt: context,
523
+ schema,
524
+ }),
525
+ new Promise((_, reject) => setTimeout(() => reject(new Error("user-profile-learning: timeout")), 120000)),
526
+ ]);
527
+ log("user-profile-learning: LLM returned", {
528
+ prefCount: result.preferences?.length,
529
+ patCount: result.patterns?.length,
530
+ wfCount: result.workflows?.length,
531
+ });
532
+ const rawData = result;
533
+ if (existingProfile) {
534
+ const existingData = JSON.parse(existingProfile.profileData);
535
+ const merged = await userProfileManager.mergeProfileData(existingData, rawData, undefined, existingProfile.id);
536
+ return { raw: rawData, merged };
537
+ }
538
+ return { raw: rawData, merged: null };
539
+ }
540
+ catch (e) {
541
+ opencodeProviderError = e;
542
+ log("user-profile-learning: opencode provider failed, falling back to external API", {
543
+ error: String(e),
544
+ });
545
+ }
546
+ }
547
+ if (!CONFIG.memoryModel || !CONFIG.memoryApiUrl) {
548
+ if (opencodeProviderError) {
549
+ throw opencodeProviderError;
550
+ }
551
+ log("User Profile Config Check Failed:", {
552
+ memoryModel: CONFIG.memoryModel,
553
+ memoryApiUrl: CONFIG.memoryApiUrl,
554
+ });
555
+ throw new Error("External API not configured for user memory learning");
556
+ }
557
+ const { AIProviderFactory } = await import("./ai/ai-provider-factory.js");
558
+ const { buildMemoryProviderConfig } = await import("./ai/provider-config.js");
559
+ const providerConfig = buildMemoryProviderConfig(CONFIG);
560
+ const provider = AIProviderFactory.createProvider(CONFIG.memoryProvider, providerConfig);
561
+ const systemPrompt = `You are a user behavior analyst for a coding assistant.
562
+
563
+ Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile.
564
+
565
+ CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts.
566
+
567
+ CRITICAL: All JSON string values MUST escape double quotes with backslash. Do NOT use unescaped quotation marks inside string values.
568
+
569
+ Use the update_user_profile tool to save the ${existingProfile ? "updated" : "new"} profile.`;
570
+ const toolSchema = createUserProfileToolSchema(Boolean(existingProfile));
571
+ const result = await provider.executeToolCall(systemPrompt, context, toolSchema, `user-profile-${Date.now()}`);
572
+ if (!result.success || !result.data) {
573
+ throw new Error(result.error || "Failed to analyze user profile");
574
+ }
575
+ const rawData = result.data;
576
+ if (existingProfile) {
577
+ const existingData = JSON.parse(existingProfile.profileData);
578
+ const merged = await userProfileManager.mergeProfileData(existingData, rawData, undefined, existingProfile.id);
579
+ return { raw: rawData, merged };
580
+ }
581
+ return { raw: rawData, merged: null };
582
+ }
583
+ async function buildLearningPaths(prompts, profileId) {
584
+ const promptTexts = prompts.map((p, i) => `${i + 1}. ${p.content}`).join("\n");
585
+ const systemPrompt = "You are a learning path analyst. Identify causal chains across a user's prompts. Output valid JSON.";
586
+ const userPrompt = `Analyze these user prompts for causal learning chains:
587
+
588
+ ${promptTexts}
589
+
590
+ Identify sequences where earlier prompts led to later ones (e.g. "learned X → applied X → refined X"). Return JSON:
591
+ { "paths": [{ "topic": "string", "chain": ["step1", "step2", "step3"], "description": "one sentence summary" }] }
592
+ If no clear chains, return { "paths": [] }.`;
593
+ let result = null;
594
+ if (CONFIG.opencodeProvider && CONFIG.opencodeModel) {
595
+ try {
596
+ const { z } = await import("zod");
597
+ const { generateStructuredOutput } = await loadOpencodeProvider();
598
+ const { getOpenCodeClient } = await import("./ai/profile-llm-client.js");
599
+ let v2Client;
600
+ try {
601
+ v2Client = await getOpenCodeClient();
602
+ }
603
+ catch {
604
+ // provider not available, skip learning paths
605
+ }
606
+ if (v2Client) {
607
+ result = (await Promise.race([
608
+ generateStructuredOutput({
609
+ client: v2Client,
610
+ providerID: CONFIG.opencodeProvider,
611
+ modelID: CONFIG.opencodeModel,
612
+ systemPrompt,
613
+ userPrompt,
614
+ schema: z.object({
615
+ paths: z.array(z.object({
616
+ topic: z.string(),
617
+ chain: z.array(z.string()),
618
+ description: z.string(),
619
+ })),
620
+ }),
621
+ }),
622
+ new Promise((_, reject) => setTimeout(() => reject(new Error("learning paths: opencode timeout")), 120000)),
623
+ ]));
624
+ }
625
+ }
626
+ catch (e) {
627
+ log("learning paths: native provider failed", { error: String(e) });
628
+ }
629
+ }
630
+ if (!result && CONFIG.memoryModel && CONFIG.memoryApiUrl) {
631
+ try {
632
+ const response = await fetch(`${CONFIG.memoryApiUrl}/chat/completions`, {
633
+ method: "POST",
634
+ headers: {
635
+ "Content-Type": "application/json",
636
+ Authorization: `Bearer ${CONFIG.memoryApiKey || ""}`,
637
+ },
638
+ body: JSON.stringify({
639
+ model: CONFIG.memoryModel,
640
+ messages: [
641
+ { role: "system", content: systemPrompt },
642
+ { role: "user", content: userPrompt },
643
+ ],
644
+ temperature: 0.3,
645
+ response_format: { type: "json_object" },
646
+ }),
647
+ signal: AbortSignal.timeout(60000),
648
+ });
649
+ if (response.ok) {
650
+ const data = (await response.json());
651
+ const content = data.choices?.[0]?.message?.content;
652
+ if (content)
653
+ result = JSON.parse(content);
654
+ }
655
+ }
656
+ catch (e) {
657
+ log("learning paths: external API failed", { error: String(e) });
658
+ }
659
+ }
660
+ if (!result?.paths?.length)
661
+ return;
662
+ log("learning paths: detected", {
663
+ profileId,
664
+ pathCount: result.paths.length,
665
+ topics: result.paths.map((p) => p.topic).join(", "),
666
+ });
667
+ const profile = await userProfileManager.getProfileById(profileId);
668
+ if (!profile)
669
+ return;
670
+ const data = JSON.parse(profile.profileData);
671
+ data.learning_paths = result.paths;
672
+ await userProfileManager.updateProfile(profileId, data, 0, `Updated learning paths: ${result.paths.map((p) => p.topic).join(", ")}`);
673
+ }