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.
- package/LICENSE +21 -0
- package/README.md +587 -0
- package/dist/adapters/opencode/auto-capture-host.d.ts +3 -0
- package/dist/adapters/opencode/auto-capture-host.js +29 -0
- package/dist/adapters/opencode/auto-capture-summary.d.ts +4 -0
- package/dist/adapters/opencode/auto-capture-summary.js +147 -0
- package/dist/adapters/opencode/conversation.d.ts +2 -0
- package/dist/adapters/opencode/conversation.js +50 -0
- package/dist/adapters/opencode/user-prompt.d.ts +9 -0
- package/dist/adapters/opencode/user-prompt.js +26 -0
- package/dist/adapters/pi/capture.d.ts +41 -0
- package/dist/adapters/pi/capture.js +91 -0
- package/dist/adapters/pi/conversation.d.ts +38 -0
- package/dist/adapters/pi/conversation.js +118 -0
- package/dist/adapters/pi/extension.d.ts +18 -0
- package/dist/adapters/pi/extension.js +234 -0
- package/dist/adapters/pi/import-command.d.ts +29 -0
- package/dist/adapters/pi/import-command.js +232 -0
- package/dist/adapters/pi/profile.d.ts +39 -0
- package/dist/adapters/pi/profile.js +148 -0
- package/dist/adapters/pi/provider.d.ts +50 -0
- package/dist/adapters/pi/provider.js +77 -0
- package/dist/config.d.ts +290 -0
- package/dist/config.js +742 -0
- package/dist/core/capture-context.d.ts +4 -0
- package/dist/core/capture-context.js +132 -0
- package/dist/core/capture.d.ts +15 -0
- package/dist/core/capture.js +68 -0
- package/dist/core/extraction.d.ts +80 -0
- package/dist/core/extraction.js +140 -0
- package/dist/core/host.d.ts +52 -0
- package/dist/core/host.js +1 -0
- package/dist/core/memory-operations.d.ts +27 -0
- package/dist/core/memory-operations.js +247 -0
- package/dist/core/retrieval.d.ts +12 -0
- package/dist/core/retrieval.js +72 -0
- package/dist/importer/discovery.d.ts +31 -0
- package/dist/importer/discovery.js +88 -0
- package/dist/importer/importer.d.ts +87 -0
- package/dist/importer/importer.js +260 -0
- package/dist/importer/ledger.d.ts +44 -0
- package/dist/importer/ledger.js +137 -0
- package/dist/importer/session-loader.d.ts +19 -0
- package/dist/importer/session-loader.js +12 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.js +601 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.js +11 -0
- package/dist/services/ai/ai-provider-factory.d.ts +7 -0
- package/dist/services/ai/ai-provider-factory.js +41 -0
- package/dist/services/ai/api-key-placeholder.d.ts +1 -0
- package/dist/services/ai/api-key-placeholder.js +14 -0
- package/dist/services/ai/internal-capture-sessions.d.ts +6 -0
- package/dist/services/ai/internal-capture-sessions.js +33 -0
- package/dist/services/ai/opencode-diagnostics.d.ts +7 -0
- package/dist/services/ai/opencode-diagnostics.js +36 -0
- package/dist/services/ai/opencode-host-config.d.ts +10 -0
- package/dist/services/ai/opencode-host-config.js +51 -0
- package/dist/services/ai/opencode-provider-loader.d.ts +3 -0
- package/dist/services/ai/opencode-provider-loader.js +4 -0
- package/dist/services/ai/opencode-provider.d.ts +92 -0
- package/dist/services/ai/opencode-provider.js +419 -0
- package/dist/services/ai/opencode-sdk-client.d.ts +6 -0
- package/dist/services/ai/opencode-sdk-client.js +38 -0
- package/dist/services/ai/profile-llm-client.d.ts +2 -0
- package/dist/services/ai/profile-llm-client.js +27 -0
- package/dist/services/ai/provider-config.d.ts +17 -0
- package/dist/services/ai/provider-config.js +30 -0
- package/dist/services/ai/providers/anthropic-messages.d.ts +34 -0
- package/dist/services/ai/providers/anthropic-messages.js +220 -0
- package/dist/services/ai/providers/base-provider.d.ts +24 -0
- package/dist/services/ai/providers/base-provider.js +24 -0
- package/dist/services/ai/providers/google-gemini.d.ts +15 -0
- package/dist/services/ai/providers/google-gemini.js +228 -0
- package/dist/services/ai/providers/minimax.d.ts +35 -0
- package/dist/services/ai/providers/minimax.js +55 -0
- package/dist/services/ai/providers/openai-chat-completion.d.ts +22 -0
- package/dist/services/ai/providers/openai-chat-completion.js +387 -0
- package/dist/services/ai/providers/openai-responses.d.ts +13 -0
- package/dist/services/ai/providers/openai-responses.js +182 -0
- package/dist/services/ai/providers/orcarouter.d.ts +51 -0
- package/dist/services/ai/providers/orcarouter.js +66 -0
- package/dist/services/ai/session/ai-session-manager.d.ts +24 -0
- package/dist/services/ai/session/ai-session-manager.js +211 -0
- package/dist/services/ai/session/session-types.d.ts +42 -0
- package/dist/services/ai/session/session-types.js +1 -0
- package/dist/services/ai/tools/tool-schema.d.ts +40 -0
- package/dist/services/ai/tools/tool-schema.js +24 -0
- package/dist/services/ai/validators/user-profile-validator.d.ts +12 -0
- package/dist/services/ai/validators/user-profile-validator.js +114 -0
- package/dist/services/api-handlers.d.ts +186 -0
- package/dist/services/api-handlers.js +1284 -0
- package/dist/services/auth-token.d.ts +14 -0
- package/dist/services/auth-token.js +59 -0
- package/dist/services/auto-capture.d.ts +3 -0
- package/dist/services/auto-capture.js +127 -0
- package/dist/services/cleanup-service.d.ts +22 -0
- package/dist/services/cleanup-service.js +109 -0
- package/dist/services/client.d.ts +134 -0
- package/dist/services/client.js +327 -0
- package/dist/services/context.d.ts +10 -0
- package/dist/services/context.js +27 -0
- package/dist/services/cors.d.ts +12 -0
- package/dist/services/cors.js +52 -0
- package/dist/services/deduplication-service.d.ts +28 -0
- package/dist/services/deduplication-service.js +111 -0
- package/dist/services/embedding.d.ts +36 -0
- package/dist/services/embedding.js +200 -0
- package/dist/services/jsonc.d.ts +6 -0
- package/dist/services/jsonc.js +76 -0
- package/dist/services/language-detector.d.ts +2 -0
- package/dist/services/language-detector.js +33 -0
- package/dist/services/legacy-migration.d.ts +77 -0
- package/dist/services/legacy-migration.js +343 -0
- package/dist/services/logger.d.ts +1 -0
- package/dist/services/logger.js +119 -0
- package/dist/services/memory-portability-service.d.ts +37 -0
- package/dist/services/memory-portability-service.js +340 -0
- package/dist/services/memory-scope.d.ts +16 -0
- package/dist/services/memory-scope.js +43 -0
- package/dist/services/migration-service.d.ts +42 -0
- package/dist/services/migration-service.js +307 -0
- package/dist/services/onnxruntime-resolve.d.ts +24 -0
- package/dist/services/onnxruntime-resolve.js +174 -0
- package/dist/services/privacy.d.ts +26 -0
- package/dist/services/privacy.js +69 -0
- package/dist/services/request-timeouts.d.ts +9 -0
- package/dist/services/request-timeouts.js +11 -0
- package/dist/services/runtime-require.d.ts +17 -0
- package/dist/services/runtime-require.js +75 -0
- package/dist/services/secret-resolver.d.ts +1 -0
- package/dist/services/secret-resolver.js +65 -0
- package/dist/services/shard-inventory-service.d.ts +69 -0
- package/dist/services/shard-inventory-service.js +315 -0
- package/dist/services/shard-path-migration-service.d.ts +31 -0
- package/dist/services/shard-path-migration-service.js +367 -0
- package/dist/services/tag-prefix-migration.d.ts +60 -0
- package/dist/services/tag-prefix-migration.js +439 -0
- package/dist/services/tag-prefix-scan.d.ts +26 -0
- package/dist/services/tag-prefix-scan.js +91 -0
- package/dist/services/tags.d.ts +29 -0
- package/dist/services/tags.js +251 -0
- package/dist/services/turso/connection-manager.d.ts +15 -0
- package/dist/services/turso/connection-manager.js +163 -0
- package/dist/services/turso/cross-process-write-lock.d.ts +1 -0
- package/dist/services/turso/cross-process-write-lock.js +89 -0
- package/dist/services/turso/legacy-migrator.d.ts +1 -0
- package/dist/services/turso/legacy-migrator.js +540 -0
- package/dist/services/turso/lifecycle.d.ts +1 -0
- package/dist/services/turso/lifecycle.js +14 -0
- package/dist/services/turso/operation-lock.d.ts +2 -0
- package/dist/services/turso/operation-lock.js +61 -0
- package/dist/services/turso/ready.d.ts +2 -0
- package/dist/services/turso/ready.js +36 -0
- package/dist/services/turso/shard-manager.d.ts +38 -0
- package/dist/services/turso/shard-manager.js +496 -0
- package/dist/services/turso/sqlite-handle-release.d.ts +16 -0
- package/dist/services/turso/sqlite-handle-release.js +56 -0
- package/dist/services/turso/turso-db.d.ts +18 -0
- package/dist/services/turso/turso-db.js +53 -0
- package/dist/services/turso/types.d.ts +42 -0
- package/dist/services/turso/types.js +1 -0
- package/dist/services/turso/vector-search.d.ts +41 -0
- package/dist/services/turso/vector-search.js +384 -0
- package/dist/services/turso/vector-utils.d.ts +7 -0
- package/dist/services/turso/vector-utils.js +67 -0
- package/dist/services/user-memory-learning.d.ts +97 -0
- package/dist/services/user-memory-learning.js +673 -0
- package/dist/services/user-profile/ai-cleanup.d.ts +58 -0
- package/dist/services/user-profile/ai-cleanup.js +486 -0
- package/dist/services/user-profile/profile-context.d.ts +1 -0
- package/dist/services/user-profile/profile-context.js +105 -0
- package/dist/services/user-profile/profile-utils.d.ts +2 -0
- package/dist/services/user-profile/profile-utils.js +45 -0
- package/dist/services/user-profile/types.d.ts +92 -0
- package/dist/services/user-profile/types.js +1 -0
- package/dist/services/user-profile/user-profile-manager.d.ts +65 -0
- package/dist/services/user-profile/user-profile-manager.js +1660 -0
- package/dist/services/user-prompt/user-prompt-manager.d.ts +53 -0
- package/dist/services/user-prompt/user-prompt-manager.js +303 -0
- package/dist/services/web-api-auth.d.ts +3 -0
- package/dist/services/web-api-auth.js +25 -0
- package/dist/services/web-auth.d.ts +46 -0
- package/dist/services/web-auth.js +105 -0
- package/dist/services/web-server.d.ts +57 -0
- package/dist/services/web-server.js +659 -0
- package/dist/shared/api/index.d.ts +4 -0
- package/dist/shared/api/index.js +2 -0
- package/dist/shared/api/portability-schemas.d.ts +56 -0
- package/dist/shared/api/portability-schemas.js +39 -0
- package/dist/shared/api/schemas.d.ts +295 -0
- package/dist/shared/api/schemas.js +134 -0
- package/dist/shared/api/types.d.ts +28 -0
- package/dist/shared/api/types.js +1 -0
- package/dist/types/index.d.ts +27 -0
- package/dist/types/index.js +1 -0
- package/dist/utils/context-limit.d.ts +10 -0
- package/dist/utils/context-limit.js +43 -0
- package/dist/utils/math.d.ts +7 -0
- package/dist/utils/math.js +48 -0
- package/dist/utils/profile.d.ts +2 -0
- package/dist/utils/profile.js +15 -0
- package/dist/v2/adapter.d.ts +5 -0
- package/dist/v2/adapter.js +218 -0
- package/dist/v2/legacy-client.d.ts +15 -0
- package/dist/v2/legacy-client.js +257 -0
- package/dist/v2/memory-bridge.d.ts +18 -0
- package/dist/v2/memory-bridge.js +29 -0
- package/dist/v2/plugin.d.ts +3 -0
- package/dist/v2/plugin.js +24 -0
- package/dist/web/assets/index-DPjEpD1h.css +2 -0
- package/dist/web/assets/index-do26c4JJ.js +122 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-wght-normal-D73BlboJ.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-wght-normal-Bw9x6K1M.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-wght-normal-DBQx-q_a.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-wght-normal-B9CIFXIH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-wght-normal-Bt-aOZkq.woff2 +0 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/index.html +14 -0
- package/dist/web/omms-icon.png +0 -0
- package/package.json +138 -0
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { UserProfileData } from "./types.js";
|
|
2
|
+
export interface AICleanupResult {
|
|
3
|
+
cleaned: UserProfileData;
|
|
4
|
+
diff: CleanupDiff;
|
|
5
|
+
}
|
|
6
|
+
export interface CleanupDiff {
|
|
7
|
+
kept: string[];
|
|
8
|
+
merged: Array<{
|
|
9
|
+
ids: string[];
|
|
10
|
+
result: string;
|
|
11
|
+
}>;
|
|
12
|
+
removed: Array<{
|
|
13
|
+
id: string;
|
|
14
|
+
reason: string;
|
|
15
|
+
}>;
|
|
16
|
+
}
|
|
17
|
+
export declare function aiCleanupProfile(profileData: UserProfileData): Promise<AICleanupResult>;
|
|
18
|
+
export declare function aiCleanupProfileFromIndexed(indexed: IndexedProfile): Promise<AICleanupResult>;
|
|
19
|
+
export declare function filterProfileForCleanup(profileData: UserProfileData, includeIds: string[]): IndexedProfile;
|
|
20
|
+
interface IndexedProfileItem {
|
|
21
|
+
id: string;
|
|
22
|
+
category?: string;
|
|
23
|
+
description: string;
|
|
24
|
+
confidence?: number;
|
|
25
|
+
frequency?: number;
|
|
26
|
+
[key: string]: unknown;
|
|
27
|
+
}
|
|
28
|
+
interface IndexedProfile {
|
|
29
|
+
preferences: IndexedProfileItem[];
|
|
30
|
+
patterns: IndexedProfileItem[];
|
|
31
|
+
workflows: IndexedProfileItem[];
|
|
32
|
+
}
|
|
33
|
+
interface AIMapping {
|
|
34
|
+
kept: string[];
|
|
35
|
+
merged: string[][];
|
|
36
|
+
removed: string[];
|
|
37
|
+
}
|
|
38
|
+
type PromptInfo = {
|
|
39
|
+
error?: {
|
|
40
|
+
name: string;
|
|
41
|
+
data?: {
|
|
42
|
+
message?: string;
|
|
43
|
+
};
|
|
44
|
+
};
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* Extract assistant text from an OpenCode session.prompt result.
|
|
48
|
+
* AssistantMessage has no `text` field; content lives in `parts` (#177).
|
|
49
|
+
*/
|
|
50
|
+
export declare function extractTextFromPromptResult(promptResult: unknown): {
|
|
51
|
+
info: PromptInfo | undefined;
|
|
52
|
+
rawText: string;
|
|
53
|
+
};
|
|
54
|
+
export declare function rebuildProfileUsing(mapping: AIMapping, cleanedById: Map<string, IndexedProfileItem>, originalById: Map<string, IndexedProfileItem>, counters?: {
|
|
55
|
+
cleaned: number;
|
|
56
|
+
original: number;
|
|
57
|
+
}): UserProfileData;
|
|
58
|
+
export {};
|
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { CONFIG } from "../../config.js";
|
|
2
|
+
import { log } from "../logger.js";
|
|
3
|
+
import { loadOpencodeProvider } from "../ai/opencode-provider-loader.js";
|
|
4
|
+
import { EXTERNAL_PROFILE_CLEANUP_TIMEOUT_MS, OPENCODE_PROFILE_CLEANUP_TIMEOUT_MS, } from "../request-timeouts.js";
|
|
5
|
+
import { applySafeExtraParams } from "../ai/providers/base-provider.js";
|
|
6
|
+
export async function aiCleanupProfile(profileData) {
|
|
7
|
+
const t0 = Date.now();
|
|
8
|
+
const indexed = addIdsToProfile(profileData);
|
|
9
|
+
const prompt = buildAICleanupPrompt(indexed);
|
|
10
|
+
log("AI cleanup: prompt built", {
|
|
11
|
+
prefCount: indexed.preferences.length,
|
|
12
|
+
patCount: indexed.patterns.length,
|
|
13
|
+
wfCount: indexed.workflows.length,
|
|
14
|
+
promptLen: prompt.length,
|
|
15
|
+
buildMs: Date.now() - t0,
|
|
16
|
+
});
|
|
17
|
+
const aiStart = Date.now();
|
|
18
|
+
const result = await callAICleanup(prompt);
|
|
19
|
+
log("AI cleanup: AI response received", { callMs: Date.now() - aiStart });
|
|
20
|
+
const cleanedById = buildItemIndex(result.profile);
|
|
21
|
+
const originalById = buildItemIndex(indexed);
|
|
22
|
+
const counters = { cleaned: 0, original: 0 };
|
|
23
|
+
const cleaned = rebuildProfileUsing(result.mapping, cleanedById, originalById, counters);
|
|
24
|
+
const diff = generateDiff(indexed, result.mapping, cleanedById);
|
|
25
|
+
const sampleCleaned = result.profile.preferences[0];
|
|
26
|
+
log("AI cleanup: rebuild done", {
|
|
27
|
+
cleanedPrefCount: result.profile.preferences.length,
|
|
28
|
+
cleanedPatCount: result.profile.patterns.length,
|
|
29
|
+
cleanedWfCount: result.profile.workflows.length,
|
|
30
|
+
sampleId: sampleCleaned?.id,
|
|
31
|
+
sampleDescLen: sampleCleaned?.description?.length,
|
|
32
|
+
cleanedItemDescLen: cleanedById.get("pref_0")?.description?.length,
|
|
33
|
+
originalItemDescLen: originalById.get("pref_0")?.description?.length,
|
|
34
|
+
keptById: cleaned.preferences.length,
|
|
35
|
+
usedCleaned: counters.cleaned,
|
|
36
|
+
usedOriginal: counters.original,
|
|
37
|
+
});
|
|
38
|
+
log("AI cleanup: complete", {
|
|
39
|
+
totalMs: Date.now() - t0,
|
|
40
|
+
kept: diff.kept.length,
|
|
41
|
+
merged: diff.merged.length,
|
|
42
|
+
removed: diff.removed.length,
|
|
43
|
+
});
|
|
44
|
+
return { cleaned, diff };
|
|
45
|
+
}
|
|
46
|
+
export async function aiCleanupProfileFromIndexed(indexed) {
|
|
47
|
+
const t0 = Date.now();
|
|
48
|
+
const prompt = buildAICleanupPrompt(indexed);
|
|
49
|
+
log("AI cleanup: prompt built (filtered)", {
|
|
50
|
+
prefCount: indexed.preferences.length,
|
|
51
|
+
patCount: indexed.patterns.length,
|
|
52
|
+
wfCount: indexed.workflows.length,
|
|
53
|
+
promptLen: prompt.length,
|
|
54
|
+
buildMs: Date.now() - t0,
|
|
55
|
+
});
|
|
56
|
+
const aiStart = Date.now();
|
|
57
|
+
const result = await callAICleanup(prompt);
|
|
58
|
+
log("AI cleanup: AI response received (filtered)", { callMs: Date.now() - aiStart });
|
|
59
|
+
const cleanedById = buildItemIndex(result.profile);
|
|
60
|
+
const originalById = buildItemIndex(indexed);
|
|
61
|
+
const counters = { cleaned: 0, original: 0 };
|
|
62
|
+
const cleaned = rebuildProfileUsing(result.mapping, cleanedById, originalById, counters);
|
|
63
|
+
const diff = generateDiff(indexed, result.mapping, cleanedById);
|
|
64
|
+
log("AI cleanup: complete (filtered)", {
|
|
65
|
+
totalMs: Date.now() - t0,
|
|
66
|
+
kept: diff.kept.length,
|
|
67
|
+
merged: diff.merged.length,
|
|
68
|
+
removed: diff.removed.length,
|
|
69
|
+
});
|
|
70
|
+
return { cleaned, diff };
|
|
71
|
+
}
|
|
72
|
+
export function filterProfileForCleanup(profileData, includeIds) {
|
|
73
|
+
const idSet = new Set(includeIds);
|
|
74
|
+
const indexed = addIdsToProfile(profileData);
|
|
75
|
+
return {
|
|
76
|
+
preferences: indexed.preferences.filter((p) => idSet.has(p.id)),
|
|
77
|
+
patterns: indexed.patterns.filter((p) => idSet.has(p.id)),
|
|
78
|
+
workflows: indexed.workflows.filter((p) => idSet.has(p.id)),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
// The model controls this JSON; it may omit fields or return wrong types. Coerce to a
|
|
82
|
+
// well-formed AIMapping so rebuildProfileUsing/generateDiff never call .map/.filter/.includes
|
|
83
|
+
// on undefined. A missing or malformed mapping degrades to a no-op cleanup (all originals are
|
|
84
|
+
// preserved as "unmentioned") rather than aborting or corrupting the profile.
|
|
85
|
+
function normalizeAIMapping(raw) {
|
|
86
|
+
if (!raw || typeof raw !== "object") {
|
|
87
|
+
log("AI cleanup: response missing valid mapping; treating as no-op", {
|
|
88
|
+
mappingType: typeof raw,
|
|
89
|
+
});
|
|
90
|
+
return { kept: [], merged: [], removed: [] };
|
|
91
|
+
}
|
|
92
|
+
const isStr = (x) => typeof x === "string";
|
|
93
|
+
const kept = Array.isArray(raw.kept) ? raw.kept.filter(isStr) : [];
|
|
94
|
+
const merged = Array.isArray(raw.merged)
|
|
95
|
+
? raw.merged
|
|
96
|
+
.filter((g) => Array.isArray(g))
|
|
97
|
+
.map((g) => g.filter(isStr))
|
|
98
|
+
.filter((g) => g.length > 0)
|
|
99
|
+
: [];
|
|
100
|
+
const removed = Array.isArray(raw.removed) ? raw.removed.filter(isStr) : [];
|
|
101
|
+
return { kept, merged, removed };
|
|
102
|
+
}
|
|
103
|
+
function addIdsToProfile(profile) {
|
|
104
|
+
const items = {
|
|
105
|
+
preferences: profile.preferences.map((p, i) => ({ ...p, id: `pref_${i}` })),
|
|
106
|
+
patterns: profile.patterns.map((p, i) => ({ ...p, id: `pat_${i}` })),
|
|
107
|
+
workflows: profile.workflows.map((w, i) => ({ ...w, id: `wf_${i}` })),
|
|
108
|
+
};
|
|
109
|
+
return items;
|
|
110
|
+
}
|
|
111
|
+
function buildAICleanupPrompt(profile) {
|
|
112
|
+
const profileJSON = JSON.stringify({
|
|
113
|
+
preferences: profile.preferences.map(formatForAI),
|
|
114
|
+
patterns: profile.patterns.map(formatForAI),
|
|
115
|
+
workflows: profile.workflows.map(formatForAI),
|
|
116
|
+
}, null, 2);
|
|
117
|
+
return `You are a user profile analyst. The profile below contains duplicate entries within each category (preferences, patterns, workflows). Output a cleaned profile.
|
|
118
|
+
|
|
119
|
+
Rules:
|
|
120
|
+
1. Merge semantically identical entries ONLY within the same section (pref_ with pref_, pat_ with pat_, wf_ with wf_)
|
|
121
|
+
2. Do NOT merge across sections — preferences and patterns are different things
|
|
122
|
+
3. When merging, keep the most specific description. Do not artificially shorten or inflate — the natural length of the original is fine.
|
|
123
|
+
4. Do not add new entries; only merge and remove
|
|
124
|
+
5. Prefer merging over removing. If entries share the same topic or describe similar behavior, merge them. Only remove truly irrelevant/generic items that add no value (e.g. "uses tools", "checks things").
|
|
125
|
+
6. You MUST return a mapping showing the disposition of each id
|
|
126
|
+
|
|
127
|
+
Current profile:
|
|
128
|
+
${profileJSON}
|
|
129
|
+
|
|
130
|
+
Return JSON only (no markdown):
|
|
131
|
+
{
|
|
132
|
+
"preferences": [
|
|
133
|
+
{ "id": "pref_0", "category": "...", "description": "..." }
|
|
134
|
+
],
|
|
135
|
+
"patterns": [...],
|
|
136
|
+
"workflows": [...],
|
|
137
|
+
"mapping": {
|
|
138
|
+
"kept": ["pref_0", "pat_1"],
|
|
139
|
+
"merged": [["pref_2", "pref_5"], ["pat_3", "pat_8"]],
|
|
140
|
+
"removed": ["pref_4"]
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
The first id in each merged group is the kept entry; the rest are merged into it.`;
|
|
145
|
+
}
|
|
146
|
+
function formatForAI(item) {
|
|
147
|
+
const { id, category, description, frequency } = item;
|
|
148
|
+
return { id, category, description, frequency };
|
|
149
|
+
}
|
|
150
|
+
async function callAICleanup(prompt) {
|
|
151
|
+
// Use opencode internal session when opencodeProvider is configured (same pattern as auto-capture).
|
|
152
|
+
// When the client is available, surface OpenCode errors instead of masking them as
|
|
153
|
+
// "No AI provider configured" via a silent fallback (#177).
|
|
154
|
+
if (CONFIG.opencodeProvider && CONFIG.opencodeModel) {
|
|
155
|
+
const { getV2Client } = await loadOpencodeProvider();
|
|
156
|
+
const v2Client = getV2Client();
|
|
157
|
+
if (v2Client) {
|
|
158
|
+
return callViaOpencodeWithClient(v2Client, prompt);
|
|
159
|
+
}
|
|
160
|
+
log("AI cleanup: opencode client unavailable, falling back to external API");
|
|
161
|
+
}
|
|
162
|
+
if (CONFIG.memoryModel && CONFIG.memoryApiUrl) {
|
|
163
|
+
return callViaExternalAPI(prompt);
|
|
164
|
+
}
|
|
165
|
+
throw new Error("No AI provider configured for profile cleanup");
|
|
166
|
+
}
|
|
167
|
+
async function callViaExternalAPI(prompt) {
|
|
168
|
+
const t0 = Date.now();
|
|
169
|
+
const systemPrompt = "You are a user profile cleanup assistant. Merge duplicate entries and return only JSON.";
|
|
170
|
+
const requestBody = {};
|
|
171
|
+
if (CONFIG.memoryExtraParams) {
|
|
172
|
+
applySafeExtraParams(requestBody, CONFIG.memoryExtraParams);
|
|
173
|
+
}
|
|
174
|
+
Object.assign(requestBody, {
|
|
175
|
+
model: CONFIG.memoryModel,
|
|
176
|
+
messages: [
|
|
177
|
+
{ role: "system", content: systemPrompt },
|
|
178
|
+
{ role: "user", content: prompt },
|
|
179
|
+
],
|
|
180
|
+
temperature: 0.3,
|
|
181
|
+
response_format: { type: "json_object" },
|
|
182
|
+
});
|
|
183
|
+
const response = await fetch(`${CONFIG.memoryApiUrl}/chat/completions`, {
|
|
184
|
+
method: "POST",
|
|
185
|
+
headers: {
|
|
186
|
+
"Content-Type": "application/json",
|
|
187
|
+
Authorization: `Bearer ${CONFIG.memoryApiKey}`,
|
|
188
|
+
},
|
|
189
|
+
body: JSON.stringify(requestBody),
|
|
190
|
+
signal: AbortSignal.timeout(EXTERNAL_PROFILE_CLEANUP_TIMEOUT_MS),
|
|
191
|
+
});
|
|
192
|
+
log("AI cleanup: external API http done", { httpMs: Date.now() - t0, status: response.status });
|
|
193
|
+
if (!response.ok) {
|
|
194
|
+
throw new Error(`API request failed: ${response.status} ${response.statusText}`);
|
|
195
|
+
}
|
|
196
|
+
const data = await response.json();
|
|
197
|
+
const content = data.choices?.[0]?.message?.content;
|
|
198
|
+
if (!content)
|
|
199
|
+
throw new Error("No content in API response");
|
|
200
|
+
log("AI cleanup: external API parsing done", {
|
|
201
|
+
totalMs: Date.now() - t0,
|
|
202
|
+
respLen: content.length,
|
|
203
|
+
});
|
|
204
|
+
const parsed = JSON.parse(content);
|
|
205
|
+
return {
|
|
206
|
+
profile: parsed,
|
|
207
|
+
mapping: normalizeAIMapping(parsed.mapping),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Extract assistant text from an OpenCode session.prompt result.
|
|
212
|
+
* AssistantMessage has no `text` field; content lives in `parts` (#177).
|
|
213
|
+
*/
|
|
214
|
+
export function extractTextFromPromptResult(promptResult) {
|
|
215
|
+
const result = promptResult;
|
|
216
|
+
const info = result?.data?.info ?? result?.info;
|
|
217
|
+
const parts = result?.data?.parts ?? result?.parts ?? [];
|
|
218
|
+
const rawText = parts
|
|
219
|
+
.filter((p) => p.type === "text" && p.text)
|
|
220
|
+
.map((p) => p.text)
|
|
221
|
+
.join("\n")
|
|
222
|
+
.trim();
|
|
223
|
+
return { info, rawText };
|
|
224
|
+
}
|
|
225
|
+
function raceWithTimeout(promise, ms, message) {
|
|
226
|
+
let timer;
|
|
227
|
+
const timeout = new Promise((_, reject) => {
|
|
228
|
+
timer = setTimeout(() => reject(new Error(message)), ms);
|
|
229
|
+
});
|
|
230
|
+
return Promise.race([promise, timeout]).finally(() => {
|
|
231
|
+
if (timer !== undefined)
|
|
232
|
+
clearTimeout(timer);
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
async function callViaOpencodeWithClient(v2Client, prompt) {
|
|
236
|
+
const t0 = Date.now();
|
|
237
|
+
const systemPrompt = "You are a user profile cleanup assistant. Merge duplicate entries and return only JSON without markdown wrapping.";
|
|
238
|
+
const created = (await raceWithTimeout(v2Client.session.create({
|
|
239
|
+
title: "omms profile cleanup",
|
|
240
|
+
directory: process.cwd(),
|
|
241
|
+
}), 30000, "session.create timeout"));
|
|
242
|
+
log("AI cleanup: session.create result", {
|
|
243
|
+
rawType: typeof created,
|
|
244
|
+
keys: Object.keys(created || {}),
|
|
245
|
+
hasData: !!created?.data,
|
|
246
|
+
dataId: created?.data?.id,
|
|
247
|
+
});
|
|
248
|
+
const sessionID = created?.data?.id || created?.id || created?.sessionID;
|
|
249
|
+
if (!sessionID)
|
|
250
|
+
throw new Error("session.create returned no session id");
|
|
251
|
+
log("AI cleanup: session created", { sessionID, createMs: Date.now() - t0 });
|
|
252
|
+
try {
|
|
253
|
+
const TIMEOUT_MS = OPENCODE_PROFILE_CLEANUP_TIMEOUT_MS;
|
|
254
|
+
const promptResult = await raceWithTimeout(v2Client.session.prompt({
|
|
255
|
+
sessionID,
|
|
256
|
+
model: {
|
|
257
|
+
providerID: CONFIG.opencodeProvider || "bs-aigw",
|
|
258
|
+
modelID: CONFIG.opencodeModel || "deepseek-v4-flash",
|
|
259
|
+
},
|
|
260
|
+
system: systemPrompt,
|
|
261
|
+
parts: [{ type: "text", text: prompt }],
|
|
262
|
+
// `noReply` suppresses assistant generation; cleanup needs the JSON reply (#177).
|
|
263
|
+
noReply: false,
|
|
264
|
+
}), TIMEOUT_MS, `opencodeClient prompt timeout after ${TIMEOUT_MS}ms`);
|
|
265
|
+
log("AI cleanup: session.prompt done", { promptMs: Date.now() - t0 });
|
|
266
|
+
const { info, rawText } = extractTextFromPromptResult(promptResult);
|
|
267
|
+
if (!info)
|
|
268
|
+
throw new Error("prompt response missing info");
|
|
269
|
+
if (info.error)
|
|
270
|
+
throw new Error(`opencode reported ${info.error.name}: ${info.error.data?.message ?? ""}`);
|
|
271
|
+
const jsonMatch = rawText.match(/\{[\s\S]*\}/);
|
|
272
|
+
if (!jsonMatch)
|
|
273
|
+
throw new Error("AI response did not contain valid JSON");
|
|
274
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
275
|
+
return {
|
|
276
|
+
profile: parsed,
|
|
277
|
+
mapping: normalizeAIMapping(parsed.mapping),
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
finally {
|
|
281
|
+
try {
|
|
282
|
+
await v2Client.session.delete({ sessionID });
|
|
283
|
+
}
|
|
284
|
+
catch {
|
|
285
|
+
// ignore cleanup failures for ephemeral sessions
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
function buildItemIndex(profile) {
|
|
290
|
+
const map = new Map();
|
|
291
|
+
for (const item of profile.preferences)
|
|
292
|
+
map.set(item.id, item);
|
|
293
|
+
for (const item of profile.patterns)
|
|
294
|
+
map.set(item.id, item);
|
|
295
|
+
for (const item of profile.workflows)
|
|
296
|
+
map.set(item.id, item);
|
|
297
|
+
return map;
|
|
298
|
+
}
|
|
299
|
+
export function rebuildProfileUsing(mapping, cleanedById, originalById, counters) {
|
|
300
|
+
const keptIds = new Set([...mapping.kept, ...mapping.merged.map((g) => g[0] ?? "")].filter(Boolean));
|
|
301
|
+
const mergedGroups = mapping.merged.filter((g) => g.length > 1);
|
|
302
|
+
const mergedSourceIds = new Set();
|
|
303
|
+
for (const group of mergedGroups) {
|
|
304
|
+
for (let i = 1; i < group.length; i++) {
|
|
305
|
+
mergedSourceIds.add(group[i] ?? "");
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
const result = { preferences: [], patterns: [], workflows: [] };
|
|
309
|
+
for (const id of keptIds) {
|
|
310
|
+
const cleanedItem = cleanedById.get(id);
|
|
311
|
+
const originalItem = originalById.get(id);
|
|
312
|
+
const item = cleanedItem || originalItem;
|
|
313
|
+
if (!item)
|
|
314
|
+
continue;
|
|
315
|
+
if (counters) {
|
|
316
|
+
if (cleanedById.has(id))
|
|
317
|
+
counters.cleaned++;
|
|
318
|
+
else
|
|
319
|
+
counters.original++;
|
|
320
|
+
}
|
|
321
|
+
const resultItem = { ...item };
|
|
322
|
+
delete resultItem.id;
|
|
323
|
+
if (originalItem) {
|
|
324
|
+
const preserveKeys = [
|
|
325
|
+
"centroid",
|
|
326
|
+
"anchor",
|
|
327
|
+
"weakHitCount",
|
|
328
|
+
"lastWeakHitAt",
|
|
329
|
+
"driftBelowCount",
|
|
330
|
+
"frequency",
|
|
331
|
+
"evidence",
|
|
332
|
+
"steps",
|
|
333
|
+
"confidence",
|
|
334
|
+
"alpha",
|
|
335
|
+
"beta",
|
|
336
|
+
"weakAlpha",
|
|
337
|
+
"weakBeta",
|
|
338
|
+
"lastMatchTime",
|
|
339
|
+
"firstSeen",
|
|
340
|
+
"pendingValidation",
|
|
341
|
+
];
|
|
342
|
+
for (const key of preserveKeys) {
|
|
343
|
+
if (originalItem[key] !== undefined && resultItem[key] === undefined) {
|
|
344
|
+
resultItem[key] = originalItem[key];
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (cleanedItem && cleanedItem.description !== originalItem.description) {
|
|
348
|
+
resultItem.centroid = undefined;
|
|
349
|
+
resultItem.anchor = undefined;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
if (mergedGroups.some((g) => g[0] === id)) {
|
|
353
|
+
// The merge accumulation below dereferences originalItem unconditionally. If the
|
|
354
|
+
// model returned a keeper id that only exists in its cleaned output (a hallucinated
|
|
355
|
+
// id absent from originalById), skip the group instead of throwing and aborting the
|
|
356
|
+
// entire cleanup run.
|
|
357
|
+
if (!originalItem)
|
|
358
|
+
continue;
|
|
359
|
+
const group = mergedGroups.find((g) => g[0] === id);
|
|
360
|
+
let bestFreq = originalItem.frequency || 0;
|
|
361
|
+
let bestCentroid = originalItem.centroid;
|
|
362
|
+
let bestAnchor = originalItem.anchor;
|
|
363
|
+
for (let i = 1; i < group.length; i++) {
|
|
364
|
+
const srcOriginal = originalById.get(group[i] ?? "");
|
|
365
|
+
if (srcOriginal) {
|
|
366
|
+
const srcFreq = srcOriginal.frequency || 0;
|
|
367
|
+
if (srcFreq > bestFreq) {
|
|
368
|
+
bestFreq = srcFreq;
|
|
369
|
+
bestCentroid = srcOriginal.centroid;
|
|
370
|
+
bestAnchor = srcOriginal.anchor;
|
|
371
|
+
}
|
|
372
|
+
if (srcOriginal.evidence) {
|
|
373
|
+
const existingEvidence = resultItem.evidence || [];
|
|
374
|
+
const merged = [
|
|
375
|
+
...new Set([...srcOriginal.evidence, ...existingEvidence]),
|
|
376
|
+
].slice(0, 10);
|
|
377
|
+
resultItem.evidence = merged;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// Take sum frequency — accumulating confirmed merges
|
|
382
|
+
let totalFreq = originalItem.frequency || 0;
|
|
383
|
+
for (let i = 1; i < group.length; i++) {
|
|
384
|
+
const srcOriginal = originalById.get(group[i] ?? "");
|
|
385
|
+
if (srcOriginal) {
|
|
386
|
+
totalFreq += srcOriginal.frequency || 0;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
resultItem.frequency = totalFreq;
|
|
390
|
+
// Accumulate alpha/beta from merged items
|
|
391
|
+
const keeperAlpha = originalItem.alpha || 1;
|
|
392
|
+
const keeperBeta = originalItem.beta || 1;
|
|
393
|
+
let mergedAlpha = keeperAlpha;
|
|
394
|
+
let mergedBeta = keeperBeta;
|
|
395
|
+
for (let i = 1; i < group.length; i++) {
|
|
396
|
+
const srcOriginal = originalById.get(group[i] ?? "");
|
|
397
|
+
if (srcOriginal) {
|
|
398
|
+
mergedAlpha += srcOriginal.alpha || 1;
|
|
399
|
+
mergedBeta += srcOriginal.beta || 1;
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
resultItem.alpha = mergedAlpha;
|
|
403
|
+
resultItem.beta = mergedBeta;
|
|
404
|
+
resultItem.lastMatchTime = Math.max(originalItem.lastMatchTime || 0, ...group.slice(1).map((id) => originalById.get(id ?? "")?.lastMatchTime || 0));
|
|
405
|
+
resultItem.lastSeen = Math.max(originalItem.lastSeen || 0, ...group.slice(1).map((id) => originalById.get(id ?? "")?.lastSeen || 0));
|
|
406
|
+
resultItem.pendingValidation =
|
|
407
|
+
!!originalItem.pendingValidation &&
|
|
408
|
+
group.slice(1).every((id) => !!originalById.get(id ?? "")?.pendingValidation);
|
|
409
|
+
let mergedWeakAlpha = originalItem.weakAlpha || 1;
|
|
410
|
+
let mergedWeakBeta = originalItem.weakBeta || 1;
|
|
411
|
+
for (let i = 1; i < group.length; i++) {
|
|
412
|
+
const srcOriginal = originalById.get(group[i] ?? "");
|
|
413
|
+
if (srcOriginal) {
|
|
414
|
+
mergedWeakAlpha += (srcOriginal.weakAlpha || 1) - 1;
|
|
415
|
+
mergedWeakBeta += (srcOriginal.weakBeta || 1) - 1;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
resultItem.weakAlpha = mergedWeakAlpha;
|
|
419
|
+
resultItem.weakBeta = mergedWeakBeta;
|
|
420
|
+
if (bestCentroid)
|
|
421
|
+
resultItem.centroid = bestCentroid;
|
|
422
|
+
if (bestAnchor)
|
|
423
|
+
resultItem.anchor = bestAnchor;
|
|
424
|
+
}
|
|
425
|
+
if (id.startsWith("pref_"))
|
|
426
|
+
result.preferences.push(resultItem);
|
|
427
|
+
else if (id.startsWith("pat_"))
|
|
428
|
+
result.patterns.push(resultItem);
|
|
429
|
+
else if (id.startsWith("wf_"))
|
|
430
|
+
result.workflows.push(resultItem);
|
|
431
|
+
}
|
|
432
|
+
const allOriginalIds = new Set();
|
|
433
|
+
for (const item of originalById.values()) {
|
|
434
|
+
if (item.id)
|
|
435
|
+
allOriginalIds.add(item.id);
|
|
436
|
+
}
|
|
437
|
+
const unmentionedIds = new Set();
|
|
438
|
+
for (const id of allOriginalIds) {
|
|
439
|
+
if (!keptIds.has(id) && !mergedSourceIds.has(id) && !mapping.removed.includes(id)) {
|
|
440
|
+
unmentionedIds.add(id);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
for (const id of unmentionedIds) {
|
|
444
|
+
const originalItem = originalById.get(id);
|
|
445
|
+
if (!originalItem)
|
|
446
|
+
continue;
|
|
447
|
+
const resultItem = { ...originalItem };
|
|
448
|
+
delete resultItem.id;
|
|
449
|
+
if (id.startsWith("pref_")) {
|
|
450
|
+
result.preferences.push(resultItem);
|
|
451
|
+
}
|
|
452
|
+
else if (id.startsWith("pat_")) {
|
|
453
|
+
result.patterns.push(resultItem);
|
|
454
|
+
}
|
|
455
|
+
else if (id.startsWith("wf_")) {
|
|
456
|
+
result.workflows.push(resultItem);
|
|
457
|
+
}
|
|
458
|
+
else if (originalItem.category) {
|
|
459
|
+
result.preferences.push(resultItem);
|
|
460
|
+
}
|
|
461
|
+
else {
|
|
462
|
+
result.preferences.push(resultItem);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
return result;
|
|
466
|
+
}
|
|
467
|
+
function generateDiff(original, mapping, cleanedById) {
|
|
468
|
+
const index = buildItemIndex(original);
|
|
469
|
+
const diff = {
|
|
470
|
+
kept: mapping.kept.map((id) => index.get(id)?.description || id),
|
|
471
|
+
merged: mapping.merged.map((group) => {
|
|
472
|
+
const first = group[0] ?? "";
|
|
473
|
+
return {
|
|
474
|
+
ids: group,
|
|
475
|
+
result: cleanedById.get(first)?.description || index.get(first)?.description || first,
|
|
476
|
+
};
|
|
477
|
+
}),
|
|
478
|
+
removed: mapping.removed.map((id) => ({
|
|
479
|
+
id,
|
|
480
|
+
reason: index.get(id)
|
|
481
|
+
? "AI determined this is a duplicate or stale entry"
|
|
482
|
+
: "Entry no longer exists",
|
|
483
|
+
})),
|
|
484
|
+
};
|
|
485
|
+
return diff;
|
|
486
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getUserProfileContext(userId: string): Promise<string | null>;
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { userProfileManager } from "./user-profile-manager.js";
|
|
2
|
+
import { CONFIG } from "../../config.js";
|
|
3
|
+
import { sortProfileItems } from "../../utils/profile.js";
|
|
4
|
+
import { log } from "../logger.js";
|
|
5
|
+
function dedupByCategory(items, topN) {
|
|
6
|
+
if (items.length <= topN)
|
|
7
|
+
return items;
|
|
8
|
+
const seen = new Set();
|
|
9
|
+
const result = [];
|
|
10
|
+
for (const item of items) {
|
|
11
|
+
if (seen.has(item.category))
|
|
12
|
+
continue;
|
|
13
|
+
seen.add(item.category);
|
|
14
|
+
result.push(item);
|
|
15
|
+
if (result.length >= topN)
|
|
16
|
+
break;
|
|
17
|
+
}
|
|
18
|
+
return result;
|
|
19
|
+
}
|
|
20
|
+
function scoreByRecency(items) {
|
|
21
|
+
const now = Date.now();
|
|
22
|
+
return [...items].sort((a, b) => {
|
|
23
|
+
const ageA = (now - (a.lastSeen || 0)) / (24 * 60 * 60 * 1000);
|
|
24
|
+
const ageB = (now - (b.lastSeen || 0)) / (24 * 60 * 60 * 1000);
|
|
25
|
+
const scoreA = (a.confidence || 0) * 0.7 + Math.exp(-ageA / 90) * 0.3;
|
|
26
|
+
const scoreB = (b.confidence || 0) * 0.7 + Math.exp(-ageB / 90) * 0.3;
|
|
27
|
+
return scoreB - scoreA;
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function escapeContextText(value) {
|
|
31
|
+
return String(value ?? "")
|
|
32
|
+
.replace(/&/g, "&")
|
|
33
|
+
.replace(/</g, "<")
|
|
34
|
+
.replace(/>/g, ">");
|
|
35
|
+
}
|
|
36
|
+
export async function getUserProfileContext(userId) {
|
|
37
|
+
const profile = await userProfileManager.getActiveProfile(userId);
|
|
38
|
+
if (!profile) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
let profileData;
|
|
42
|
+
try {
|
|
43
|
+
profileData = JSON.parse(profile.profileData);
|
|
44
|
+
}
|
|
45
|
+
catch (e) {
|
|
46
|
+
// A single corrupt row must not break context injection for everyone.
|
|
47
|
+
log("profile context: failed to parse stored profileData", { error: String(e) });
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
const parts = [];
|
|
51
|
+
const injectPrefs = CONFIG.userProfileInjectPreferences ?? 5;
|
|
52
|
+
const injectPats = CONFIG.userProfileInjectPatterns ?? 5;
|
|
53
|
+
const injectWfs = CONFIG.userProfileInjectWorkflows ?? 3;
|
|
54
|
+
const sortedPrefs = profileData.preferences.length > 0
|
|
55
|
+
? sortProfileItems(profileData.preferences, "confidence")
|
|
56
|
+
: [];
|
|
57
|
+
const sortedPats = profileData.patterns.length > 0
|
|
58
|
+
? sortProfileItems(profileData.patterns, "frequency")
|
|
59
|
+
: [];
|
|
60
|
+
const sortedWfs = profileData.workflows.length > 0
|
|
61
|
+
? sortProfileItems(profileData.workflows, "frequency")
|
|
62
|
+
: [];
|
|
63
|
+
const topPrefs = dedupByCategory(scoreByRecency(sortedPrefs.slice(0, injectPrefs * 2)), injectPrefs);
|
|
64
|
+
const topPats = dedupByCategory(sortedPats, injectPats);
|
|
65
|
+
const topWfs = sortedWfs.slice(0, injectWfs);
|
|
66
|
+
if (topPrefs.length > 0) {
|
|
67
|
+
parts.push("User Preferences:");
|
|
68
|
+
topPrefs.forEach((pref) => {
|
|
69
|
+
parts.push(`- [${escapeContextText(pref.category)}] ${escapeContextText(pref.description)}`);
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
if (topPats.length > 0) {
|
|
73
|
+
parts.push("\nUser Patterns:");
|
|
74
|
+
topPats.forEach((pattern) => {
|
|
75
|
+
parts.push(`- [${escapeContextText(pattern.category)}] ${escapeContextText(pattern.description)}`);
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
if (topWfs.length > 0) {
|
|
79
|
+
parts.push("\nUser Workflows:");
|
|
80
|
+
topWfs.forEach((workflow) => {
|
|
81
|
+
const steps = workflow.steps?.length
|
|
82
|
+
? ` (${workflow.frequency || 1}x: ${workflow.steps.join(" → ")})`
|
|
83
|
+
: ` (${workflow.frequency || 1}x)`;
|
|
84
|
+
parts.push(`- ${escapeContextText(workflow.description)}${escapeContextText(steps)}`);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
if (profileData.learning_paths?.length > 0) {
|
|
88
|
+
parts.push("\nLearning Paths:");
|
|
89
|
+
profileData.learning_paths.slice(0, 3).forEach((path) => {
|
|
90
|
+
parts.push(`- ${escapeContextText(path.topic)}: ${escapeContextText(path.description)}`);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if (parts.length === 0) {
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
const text = parts.join("\n");
|
|
97
|
+
if (topPrefs.length + topPats.length + topWfs.length > 0) {
|
|
98
|
+
log("profile inject", {
|
|
99
|
+
prefs: topPrefs.map((p) => `[${p.category}] ${p.description}`.substring(0, 80)),
|
|
100
|
+
pats: topPats.map((p) => `[${p.category}] ${p.description}`.substring(0, 80)),
|
|
101
|
+
wfs: topWfs.map((w) => w.description.substring(0, 80)),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
return text;
|
|
105
|
+
}
|