om-memory-system 4.18.0 → 4.19.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/adapters/opencode/profile-learning.js +7 -6
- package/dist/adapters/pi/profile.js +2 -1
- package/dist/cli/profile-normalise-categories-command.js +1 -1
- package/dist/core/memory-operations.js +10 -2
- package/dist/importer/claude-hook-api.js +2 -2
- package/dist/importer/profile-backlog.d.ts +1 -1
- package/dist/importer/profile-backlog.js +3 -2
- package/dist/importer/profile-import.d.ts +1 -1
- package/dist/services/api-handlers.js +4 -4
- package/dist/services/profile-admin.d.ts +1 -1
- package/dist/services/profile-admin.js +1 -1
- package/dist/services/user-profile/merge-and-save.d.ts +25 -0
- package/dist/services/user-profile/merge-and-save.js +38 -0
- package/dist/services/user-profile/user-profile-manager.d.ts +2 -2
- package/dist/services/user-profile/user-profile-manager.js +2 -2
- package/dist/web/assets/index-BkHg0H-_.js +127 -0
- package/dist/web/assets/index-D2yC6v1B.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BktRr9cC.css +0 -2
- package/dist/web/assets/index-CIODmA62.js +0 -127
|
@@ -5,6 +5,7 @@ import { CONFIG, refreshConfigIfChanged } from "../../config.js";
|
|
|
5
5
|
import { resolveOpencodeHostModel } from "../../services/ai/live-model-choice.js";
|
|
6
6
|
import { userPromptManager } from "../../services/user-prompt/user-prompt-manager.js";
|
|
7
7
|
import { userProfileManager } from "../../services/user-profile/user-profile-manager.js";
|
|
8
|
+
import { saveProfile } from "../../services/user-profile/merge-and-save.js";
|
|
8
9
|
import { sortProfileItems } from "../../utils/profile.js";
|
|
9
10
|
import { truncateToMaxBytes } from "../../utils/context-limit.js";
|
|
10
11
|
import { loadOpencodeProvider } from "./opencode-provider-loader.js";
|
|
@@ -29,7 +30,8 @@ async function runAutomaticProfileCleanup(userId) {
|
|
|
29
30
|
const result = await aiCleanupProfile(profileData);
|
|
30
31
|
if (result.diff.merged.length === 0 && result.diff.removed.length === 0)
|
|
31
32
|
return;
|
|
32
|
-
const
|
|
33
|
+
const summary = `Automatic AI cleanup: ${result.diff.merged.length} merged, ${result.diff.removed.length} removed`;
|
|
34
|
+
const updated = await saveProfile(profile, result.cleaned, summary);
|
|
33
35
|
log("user-profile-learning: automatic cleanup complete", {
|
|
34
36
|
userId,
|
|
35
37
|
updated,
|
|
@@ -177,7 +179,7 @@ Rules:
|
|
|
177
179
|
if (validationSummary) {
|
|
178
180
|
changeSummary = changeSummary + "; " + validationSummary;
|
|
179
181
|
}
|
|
180
|
-
success = await
|
|
182
|
+
success = await saveProfile(existingProfile, updatedProfileData, changeSummary, prompts.length);
|
|
181
183
|
if (!success) {
|
|
182
184
|
log("User profile update conflict, retrying", {
|
|
183
185
|
profileId: existingProfile.id,
|
|
@@ -188,13 +190,12 @@ Rules:
|
|
|
188
190
|
retries++;
|
|
189
191
|
}
|
|
190
192
|
if (!success) {
|
|
191
|
-
//
|
|
192
|
-
// (token burn + repeated toasts on every subsequent idle).
|
|
193
|
+
// Keep the prompts for a later run; the backoff stops a retry on every idle.
|
|
193
194
|
log("User profile update conflict: exhausted retries", {
|
|
194
195
|
profileId: existingProfile?.id,
|
|
195
196
|
userId,
|
|
196
197
|
});
|
|
197
|
-
|
|
198
|
+
recordProfileFailure("opencode", "user-profile-learning: conflict", new Error("conflict"));
|
|
198
199
|
return;
|
|
199
200
|
}
|
|
200
201
|
await userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
|
|
@@ -664,5 +665,5 @@ If no clear chains, return { "paths": [] }.`;
|
|
|
664
665
|
return;
|
|
665
666
|
const data = JSON.parse(profile.profileData);
|
|
666
667
|
data.learning_paths = result.paths;
|
|
667
|
-
await
|
|
668
|
+
await saveProfile(profile, data, `Updated learning paths: ${result.paths.map((p) => p.topic).join(", ")}`);
|
|
668
669
|
}
|
|
@@ -62,6 +62,7 @@ export async function performPiProfileLearning(input) {
|
|
|
62
62
|
}
|
|
63
63
|
const userId = tags.user.userEmail;
|
|
64
64
|
const { userProfileManager } = await import("../../services/user-profile/user-profile-manager.js");
|
|
65
|
+
const { saveMergedProfile } = await import("../../services/user-profile/merge-and-save.js");
|
|
65
66
|
const existing = await userProfileManager.getActiveProfile(userId);
|
|
66
67
|
const resolveProfileModel = input.resolveProfileModel;
|
|
67
68
|
const analyzer = resolveProfileModel
|
|
@@ -79,7 +80,7 @@ export async function performPiProfileLearning(input) {
|
|
|
79
80
|
return;
|
|
80
81
|
}
|
|
81
82
|
if (existing && analysis.merged) {
|
|
82
|
-
const updated = await userProfileManager
|
|
83
|
+
const updated = await saveMergedProfile(userProfileManager, existing, analysis, input.prompts.length, `Pi analysis of ${input.prompts.length} prompts`);
|
|
83
84
|
if (!updated) {
|
|
84
85
|
log("pi profile learning: update conflict", { profileId: existing.id });
|
|
85
86
|
return;
|
|
@@ -69,7 +69,7 @@ export async function runProfileNormaliseCategoriesCommand(argv, print = console
|
|
|
69
69
|
print("Preview only. Run again with --yes to save.");
|
|
70
70
|
return 0;
|
|
71
71
|
}
|
|
72
|
-
const saved = await userProfileManager.updateProfile(profile.id, after, 0, `Normalised profile categories: ${changes} items renamed
|
|
72
|
+
const saved = await userProfileManager.updateProfile(profile.id, after, 0, `Normalised profile categories: ${changes} items renamed`, profile.version);
|
|
73
73
|
if (!saved)
|
|
74
74
|
throw new Error("Profile was modified by another session. Please retry.");
|
|
75
75
|
print("Category renames saved. The previous snapshot remains in the profile changelog.");
|
|
@@ -145,6 +145,7 @@ export async function executeMemoryOperation(args, context) {
|
|
|
145
145
|
};
|
|
146
146
|
}
|
|
147
147
|
const { userProfileManager } = await import("../services/user-profile/user-profile-manager.js");
|
|
148
|
+
const { saveMergedProfile } = await import("../services/user-profile/merge-and-save.js");
|
|
148
149
|
const userId = tags.user.userEmail || "unknown";
|
|
149
150
|
if (args.content !== undefined) {
|
|
150
151
|
const trimmed = args.content.trim();
|
|
@@ -172,8 +173,15 @@ export async function executeMemoryOperation(args, context) {
|
|
|
172
173
|
const existingProfile = await userProfileManager.getActiveProfile(userId);
|
|
173
174
|
if (existingProfile) {
|
|
174
175
|
const existingData = JSON.parse(existingProfile.profileData);
|
|
175
|
-
const
|
|
176
|
-
await userProfileManager.
|
|
176
|
+
const raw = { preferences: [newPreference] };
|
|
177
|
+
const mergedData = await userProfileManager.mergeProfileData(existingData, raw, undefined, existingProfile.id);
|
|
178
|
+
const saved = await saveMergedProfile(userProfileManager, existingProfile, { raw, merged: mergedData }, 0, `Explicit preference added: ${sanitizedContent.slice(0, 80)}`);
|
|
179
|
+
if (!saved) {
|
|
180
|
+
return {
|
|
181
|
+
success: false,
|
|
182
|
+
error: "The profile changed while saving. Try again.",
|
|
183
|
+
};
|
|
184
|
+
}
|
|
177
185
|
return { success: true, message: "Preference saved to profile" };
|
|
178
186
|
}
|
|
179
187
|
await userProfileManager.createProfile(userId, tags.user.displayName || userId, tags.user.userName || userId, tags.user.userEmail || userId, { preferences: [newPreference], patterns: [], workflows: [] }, 0);
|
|
@@ -449,8 +449,8 @@ async function learnProfile(directory, deps) {
|
|
|
449
449
|
const context = batch.map((prompt, index) => `${index + 1}. ${prompt.content}`).join("\n");
|
|
450
450
|
const analysis = await analyzeProfile(model, context, existing ? { id: existing.id, profileData: existing.profileData } : null);
|
|
451
451
|
if (existing) {
|
|
452
|
-
const
|
|
453
|
-
|
|
452
|
+
const { saveMergedProfile } = await import("../services/user-profile/merge-and-save.js");
|
|
453
|
+
const updated = await saveMergedProfile(userProfileManager, existing, analysis, batch.length, `Claude Code analysis of ${batch.length} prompts`);
|
|
454
454
|
if (!updated)
|
|
455
455
|
throw new Error("Profile update conflict");
|
|
456
456
|
}
|
|
@@ -3,7 +3,7 @@ import { type ProfileFailureCode } from "../core/profile-failure.js";
|
|
|
3
3
|
import { userProfileManager } from "../services/user-profile/user-profile-manager.js";
|
|
4
4
|
import { userPromptManager } from "../services/user-prompt/user-prompt-manager.js";
|
|
5
5
|
export type BacklogPromptStore = Pick<typeof userPromptManager, "countUnanalyzedForUserLearning" | "getPromptsForUserLearning" | "markMultipleAsUserLearningCaptured"> & Partial<Pick<typeof userPromptManager, "skipTrivialPromptsForLearning">>;
|
|
6
|
-
export type BacklogProfileStore = Pick<typeof userProfileManager, "getActiveProfile" | "createProfile" | "updateProfile">;
|
|
6
|
+
export type BacklogProfileStore = Pick<typeof userProfileManager, "getActiveProfile" | "getProfileById" | "createProfile" | "mergeProfileData" | "updateProfile">;
|
|
7
7
|
export interface DrainProfileBacklogOptions {
|
|
8
8
|
user: {
|
|
9
9
|
userEmail: string;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { analyzeProfile } from "../core/profile-analysis.js";
|
|
2
2
|
import { profileFailureCode } from "../core/profile-failure.js";
|
|
3
|
+
import { saveMergedProfile } from "../services/user-profile/merge-and-save.js";
|
|
3
4
|
import { userProfileManager } from "../services/user-profile/user-profile-manager.js";
|
|
4
5
|
import { userPromptManager } from "../services/user-prompt/user-prompt-manager.js";
|
|
5
6
|
/**
|
|
@@ -54,8 +55,8 @@ export async function drainProfileBacklog(options) {
|
|
|
54
55
|
const context = batch.map((item, index) => `${index + 1}. ${item.content}`).join("\n");
|
|
55
56
|
const analysis = await analyzeProfile(model, context, existing ? { id: existing.id, profileData: existing.profileData } : null);
|
|
56
57
|
if (existing) {
|
|
57
|
-
|
|
58
|
-
|
|
58
|
+
const summary = `${label} of ${batch.length} prompts`;
|
|
59
|
+
if (!(await saveMergedProfile(profiles, existing, analysis, batch.length, summary)))
|
|
59
60
|
throw new Error("Profile update conflict");
|
|
60
61
|
}
|
|
61
62
|
else {
|
|
@@ -13,7 +13,7 @@ export interface ProfileImportReport {
|
|
|
13
13
|
error?: string;
|
|
14
14
|
}
|
|
15
15
|
type PromptStore = Pick<typeof userPromptManager, "savePrompt" | "markAsCaptured" | "countUnanalyzedForUserLearning" | "getPromptsForUserLearning" | "markMultipleAsUserLearningCaptured" | "markForUserLearning">;
|
|
16
|
-
type ProfileStore = Pick<typeof userProfileManager, "getActiveProfile" | "createProfile" | "updateProfile">;
|
|
16
|
+
type ProfileStore = Pick<typeof userProfileManager, "getActiveProfile" | "getProfileById" | "createProfile" | "mergeProfileData" | "updateProfile">;
|
|
17
17
|
type Ledger = Pick<ImportLedger, "get" | "begin" | "complete"> & Partial<Pick<ImportLedger, "peek">>;
|
|
18
18
|
export interface ProfileImportOptions {
|
|
19
19
|
host: MemoryHost;
|
|
@@ -9,6 +9,7 @@ import { log } from "./logger.js";
|
|
|
9
9
|
import { countKeywords } from "./keyword-counts.js";
|
|
10
10
|
import { CONFIG } from "../config.js";
|
|
11
11
|
import { userPromptManager } from "./user-prompt/user-prompt-manager.js";
|
|
12
|
+
import { saveProfile } from "./user-profile/merge-and-save.js";
|
|
12
13
|
import { sortProfileItems, stripProfileVectors } from "../utils/profile.js";
|
|
13
14
|
import { resolveWebProfileUserId } from "./profile-identity.js";
|
|
14
15
|
async function getAllMemoryShards() {
|
|
@@ -863,8 +864,7 @@ export async function handleRefreshProfile(userId) {
|
|
|
863
864
|
const pData = JSON.parse(profile.profileData);
|
|
864
865
|
const { data: decayed, hasChanges } = userProfileManager.decayInMemory(pData);
|
|
865
866
|
if (hasChanges) {
|
|
866
|
-
await
|
|
867
|
-
decayApplied = true;
|
|
867
|
+
decayApplied = await saveProfile(profile, decayed, "Applied confidence decay");
|
|
868
868
|
}
|
|
869
869
|
}
|
|
870
870
|
const unanalyzedCount = await userPromptManager.countUnanalyzedForUserLearning();
|
|
@@ -1054,7 +1054,7 @@ export async function handleApplyCleanup(userId, body) {
|
|
|
1054
1054
|
explicitAcceptance,
|
|
1055
1055
|
});
|
|
1056
1056
|
const partial = (pending.includeIds && pending.includeIds.length > 0) || explicitAcceptance;
|
|
1057
|
-
const success = await
|
|
1057
|
+
const success = await saveProfile(profile, result, partial ? "AI cleanup applied (partial)" : "AI cleanup applied");
|
|
1058
1058
|
if (!success) {
|
|
1059
1059
|
return { success: false, error: "Profile was modified by another session. Please retry." };
|
|
1060
1060
|
}
|
|
@@ -1243,7 +1243,7 @@ export async function handleUpdateProfileItem(body) {
|
|
|
1243
1243
|
const changeSummary = action === "delete"
|
|
1244
1244
|
? `Deleted ${type.slice(0, -1)} at index ${index}`
|
|
1245
1245
|
: `Edited ${type.slice(0, -1)} at index ${index}`;
|
|
1246
|
-
const success = await
|
|
1246
|
+
const success = await saveProfile(profile, profileData, changeSummary);
|
|
1247
1247
|
if (!success)
|
|
1248
1248
|
return { success: false, error: "Profile was modified by another session. Please retry." };
|
|
1249
1249
|
return {
|
|
@@ -4,7 +4,7 @@ export interface ProfileAdminStore {
|
|
|
4
4
|
getAllActiveProfiles(): Promise<UserProfile[]>;
|
|
5
5
|
getProfileById(id: string): Promise<UserProfile | null>;
|
|
6
6
|
mergeProfileData(existing: UserProfileData, updates: Partial<UserProfileData>, embedService?: undefined, profileId?: string): Promise<UserProfileData>;
|
|
7
|
-
updateProfile(profileId: string, profileData: UserProfileData, additionalPromptsAnalyzed: number, changeSummary: string): Promise<boolean>;
|
|
7
|
+
updateProfile(profileId: string, profileData: UserProfileData, additionalPromptsAnalyzed: number, changeSummary: string, expectedVersion: number): Promise<boolean>;
|
|
8
8
|
deactivateProfile(profileId: string): Promise<boolean>;
|
|
9
9
|
}
|
|
10
10
|
export interface ProfileSummary {
|
|
@@ -44,7 +44,7 @@ export async function mergeProfiles(store, sourceId, targetId) {
|
|
|
44
44
|
throw new ProfileAdminError("Both profiles must exist and be active", 404);
|
|
45
45
|
}
|
|
46
46
|
const merged = await store.mergeProfileData(parse(target), parse(source), undefined, target.id);
|
|
47
|
-
const saved = await store.updateProfile(target.id, merged, source.totalPromptsAnalyzed, `Merged profile ${source.userId}
|
|
47
|
+
const saved = await store.updateProfile(target.id, merged, source.totalPromptsAnalyzed, `Merged profile ${source.userId}`, target.version);
|
|
48
48
|
if (!saved)
|
|
49
49
|
throw new ProfileAdminError("Profile was changed by another session. Retry.", 409);
|
|
50
50
|
await store.deactivateProfile(source.id);
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { UserProfile, UserProfileData } from "./types.js";
|
|
2
|
+
/** Learning makes this many save attempts before it leaves the prompts for a later run. */
|
|
3
|
+
export declare const MAX_PROFILE_SAVE_ATTEMPTS = 3;
|
|
4
|
+
/** The profile store operations that a save needs. */
|
|
5
|
+
export interface ProfileSaveStore {
|
|
6
|
+
getProfileById(id: string): Promise<UserProfile | null>;
|
|
7
|
+
mergeProfileData(existing: UserProfileData, updates: Partial<UserProfileData>, embedService?: undefined, profileId?: string): Promise<UserProfileData>;
|
|
8
|
+
updateProfile(profileId: string, profileData: UserProfileData, additionalPromptsAnalyzed: number, changeSummary: string, expectedVersion: number): Promise<boolean>;
|
|
9
|
+
}
|
|
10
|
+
type ReadProfile = Pick<UserProfile, "id" | "version">;
|
|
11
|
+
/**
|
|
12
|
+
* Saves `data` over the profile as the caller read it. Returns false, and changes nothing,
|
|
13
|
+
* when another save landed since. The log holds a code only, never profile content.
|
|
14
|
+
*/
|
|
15
|
+
export declare function saveProfile(profile: ReadProfile, data: UserProfileData, summary: string, promptsAnalyzed?: number, store?: Pick<ProfileSaveStore, "updateProfile">): Promise<boolean>;
|
|
16
|
+
/**
|
|
17
|
+
* Saves the merged result of a profile analysis. When another save landed first, it loads
|
|
18
|
+
* the newer profile, merges the same model result into it, and saves again. It never calls
|
|
19
|
+
* the model. Returns false when there is nothing to save or every attempt met a newer version.
|
|
20
|
+
*/
|
|
21
|
+
export declare function saveMergedProfile(store: ProfileSaveStore, profile: ReadProfile, analysis: {
|
|
22
|
+
raw: Partial<UserProfileData>;
|
|
23
|
+
merged: UserProfileData | null;
|
|
24
|
+
}, promptsAnalyzed: number, summary: string): Promise<boolean>;
|
|
25
|
+
export {};
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { log } from "../logger.js";
|
|
2
|
+
/** Learning makes this many save attempts before it leaves the prompts for a later run. */
|
|
3
|
+
export const MAX_PROFILE_SAVE_ATTEMPTS = 3;
|
|
4
|
+
/**
|
|
5
|
+
* Saves `data` over the profile as the caller read it. Returns false, and changes nothing,
|
|
6
|
+
* when another save landed since. The log holds a code only, never profile content.
|
|
7
|
+
*/
|
|
8
|
+
export async function saveProfile(profile, data, summary, promptsAnalyzed = 0, store) {
|
|
9
|
+
const target = store ?? (await import("./user-profile-manager.js")).userProfileManager;
|
|
10
|
+
const saved = await target.updateProfile(profile.id, data, promptsAnalyzed, summary, profile.version);
|
|
11
|
+
if (!saved)
|
|
12
|
+
log("profile save skipped", { code: "version-changed" });
|
|
13
|
+
return saved;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Saves the merged result of a profile analysis. When another save landed first, it loads
|
|
17
|
+
* the newer profile, merges the same model result into it, and saves again. It never calls
|
|
18
|
+
* the model. Returns false when there is nothing to save or every attempt met a newer version.
|
|
19
|
+
*/
|
|
20
|
+
export async function saveMergedProfile(store, profile, analysis, promptsAnalyzed, summary) {
|
|
21
|
+
let merged = analysis.merged;
|
|
22
|
+
let version = profile.version;
|
|
23
|
+
for (let attempt = 1; merged && attempt <= MAX_PROFILE_SAVE_ATTEMPTS; attempt++) {
|
|
24
|
+
if (await store.updateProfile(profile.id, merged, promptsAnalyzed, summary, version)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
log("profile save conflict", { attempt, of: MAX_PROFILE_SAVE_ATTEMPTS });
|
|
28
|
+
if (attempt === MAX_PROFILE_SAVE_ATTEMPTS)
|
|
29
|
+
break;
|
|
30
|
+
const fresh = await store.getProfileById(profile.id);
|
|
31
|
+
if (!fresh)
|
|
32
|
+
return false;
|
|
33
|
+
const current = JSON.parse(fresh.profileData);
|
|
34
|
+
merged = await store.mergeProfileData(current, analysis.raw, undefined, fresh.id);
|
|
35
|
+
version = fresh.version;
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
@@ -20,8 +20,8 @@ export declare class UserProfileManager {
|
|
|
20
20
|
getActiveProfile(userId: string): Promise<UserProfile | null>;
|
|
21
21
|
createProfile(userId: string, displayName: string, userName: string, userEmail: string, profileData: UserProfileData, promptsAnalyzed: number): Promise<string>;
|
|
22
22
|
updateProfile(profileId: string, profileData: UserProfileData, additionalPromptsAnalyzed: number, changeSummary: string,
|
|
23
|
-
/**
|
|
24
|
-
expectedVersion
|
|
23
|
+
/** The version the caller read. The save is refused when the stored version differs. */
|
|
24
|
+
expectedVersion: number): Promise<boolean>;
|
|
25
25
|
private addChangelog;
|
|
26
26
|
private cleanupOldChangelogs;
|
|
27
27
|
getProfileChangelogs(profileId: string, limit?: number): Promise<UserProfileChangelog[]>;
|
|
@@ -274,7 +274,7 @@ export class UserProfileManager {
|
|
|
274
274
|
return id;
|
|
275
275
|
}
|
|
276
276
|
async updateProfile(profileId, profileData, additionalPromptsAnalyzed, changeSummary,
|
|
277
|
-
/**
|
|
277
|
+
/** The version the caller read. The save is refused when the stored version differs. */
|
|
278
278
|
expectedVersion) {
|
|
279
279
|
const db = await this.ready();
|
|
280
280
|
const now = Date.now();
|
|
@@ -286,7 +286,7 @@ export class UserProfileManager {
|
|
|
286
286
|
});
|
|
287
287
|
const versionRow = await db.get(`SELECT version FROM user_profiles WHERE id = ?`, [profileId]);
|
|
288
288
|
const currentVersion = Number(versionRow?.version ?? 0);
|
|
289
|
-
if (
|
|
289
|
+
if (currentVersion !== expectedVersion)
|
|
290
290
|
return false;
|
|
291
291
|
const newVersion = currentVersion + 1;
|
|
292
292
|
const changes = await db.run(`
|