om-memory-system 4.20.0 → 4.23.0-next.144
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.d.ts +0 -1
- package/dist/adapters/opencode/profile-learning.js +9 -35
- package/dist/adapters/pi/profile.js +17 -0
- package/dist/config.d.ts +1 -1
- package/dist/config.js +5 -4
- package/dist/importer/claude-hook-api.js +4 -66
- package/dist/importer/claude-profile-learning.d.ts +7 -0
- package/dist/importer/claude-profile-learning.js +78 -0
- package/dist/services/ai-cleanup-handlers.js +15 -15
- package/dist/services/auto-cleanup-handlers.d.ts +22 -0
- package/dist/services/auto-cleanup-handlers.js +71 -0
- package/dist/services/cleanup-service.js +9 -0
- package/dist/services/global-config-writer.js +15 -1
- package/dist/services/language-detector.js +14 -0
- package/dist/services/profile-cleanup-routes.d.ts +6 -0
- package/dist/services/profile-cleanup-routes.js +32 -0
- package/dist/services/settings-snapshot.js +5 -0
- package/dist/services/user-profile/ai-cleanup-apply.d.ts +21 -1
- package/dist/services/user-profile/ai-cleanup-apply.js +19 -5
- package/dist/services/user-profile/ai-cleanup-batches.d.ts +3 -2
- package/dist/services/user-profile/ai-cleanup-batches.js +5 -3
- package/dist/services/user-profile/ai-cleanup-undo.d.ts +15 -0
- package/dist/services/user-profile/ai-cleanup-undo.js +63 -0
- package/dist/services/user-profile/ai-cleanup.d.ts +2 -6
- package/dist/services/user-profile/ai-cleanup.js +6 -44
- package/dist/services/user-profile/auto-cleanup-store.d.ts +32 -0
- package/dist/services/user-profile/auto-cleanup-store.js +97 -0
- package/dist/services/user-profile/auto-cleanup-trigger.d.ts +10 -0
- package/dist/services/user-profile/auto-cleanup-trigger.js +12 -0
- package/dist/services/user-profile/auto-cleanup.d.ts +32 -0
- package/dist/services/user-profile/auto-cleanup.js +102 -0
- package/dist/services/user-profile/profile-edit-save.d.ts +35 -0
- package/dist/services/user-profile/profile-edit-save.js +26 -0
- package/dist/services/web-server.js +8 -14
- package/dist/web/assets/index-7385lY75.css +2 -0
- package/dist/web/assets/index-C9y78EH_.js +127 -0
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-CYcRfxr4.css +0 -2
- package/dist/web/assets/index-DvNL0vOb.js +0 -127
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import type { PluginInput } from "@opencode-ai/plugin";
|
|
2
|
-
export declare function shouldRunAutomaticProfileCleanup(previousPromptCount: number, addedPromptCount: number, interval?: number): boolean;
|
|
3
2
|
export declare function performUserProfileLearning(ctx: PluginInput, directory: string): Promise<void>;
|
|
4
3
|
import { createUserProfileAnalysisSchema, USER_PROFILE_LLM_CONFIDENCE_MAX } from "../../core/extraction.js";
|
|
5
4
|
export { createUserProfileAnalysisSchema, USER_PROFILE_LLM_CONFIDENCE_MAX };
|
|
@@ -6,43 +6,11 @@ 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
8
|
import { saveProfile } from "../../services/user-profile/merge-and-save.js";
|
|
9
|
+
import { shouldStartAutoCleanup } from "../../services/user-profile/auto-cleanup-trigger.js";
|
|
9
10
|
import { sortProfileItems } from "../../utils/profile.js";
|
|
10
11
|
import { truncateToMaxBytes } from "../../utils/context-limit.js";
|
|
11
12
|
import { loadOpencodeProvider } from "./opencode-provider-loader.js";
|
|
12
13
|
let isLearningRunning = false;
|
|
13
|
-
export function shouldRunAutomaticProfileCleanup(previousPromptCount, addedPromptCount, interval = CONFIG.userProfileAutoCleanupInterval) {
|
|
14
|
-
if (!CONFIG.userProfileAutoCleanupEnabled || !Number.isInteger(interval) || interval <= 0) {
|
|
15
|
-
return false;
|
|
16
|
-
}
|
|
17
|
-
return (Math.floor(previousPromptCount / interval) <
|
|
18
|
-
Math.floor((previousPromptCount + addedPromptCount) / interval));
|
|
19
|
-
}
|
|
20
|
-
async function runAutomaticProfileCleanup(userId) {
|
|
21
|
-
try {
|
|
22
|
-
const profile = await userProfileManager.getActiveProfile(userId);
|
|
23
|
-
if (!profile)
|
|
24
|
-
return;
|
|
25
|
-
const profileData = JSON.parse(profile.profileData);
|
|
26
|
-
const itemCount = profileData.preferences.length + profileData.patterns.length + profileData.workflows.length;
|
|
27
|
-
if (itemCount < 2)
|
|
28
|
-
return;
|
|
29
|
-
const { aiCleanupProfile } = await import("../../services/user-profile/ai-cleanup.js");
|
|
30
|
-
const result = await aiCleanupProfile(profileData);
|
|
31
|
-
if (result.diff.merged.length === 0 && result.diff.removed.length === 0)
|
|
32
|
-
return;
|
|
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);
|
|
35
|
-
log("user-profile-learning: automatic cleanup complete", {
|
|
36
|
-
userId,
|
|
37
|
-
updated,
|
|
38
|
-
merged: result.diff.merged.length,
|
|
39
|
-
removed: result.diff.removed.length,
|
|
40
|
-
});
|
|
41
|
-
}
|
|
42
|
-
catch (error) {
|
|
43
|
-
log("user-profile-learning: automatic cleanup failed", { userId, error: String(error) });
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
14
|
export async function performUserProfileLearning(ctx, directory) {
|
|
47
15
|
if (isLearningRunning)
|
|
48
16
|
return;
|
|
@@ -204,8 +172,14 @@ Rules:
|
|
|
204
172
|
await userProfileManager.createProfile(userId, tags.user.displayName || "Unknown", tags.user.userName || "unknown", tags.user.userEmail || "unknown", llmResult, prompts.length);
|
|
205
173
|
await userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
|
|
206
174
|
}
|
|
207
|
-
if (
|
|
208
|
-
|
|
175
|
+
if (shouldStartAutoCleanup(cleanupPreviousPromptCount, prompts.length, {
|
|
176
|
+
enabled: CONFIG.userProfileAutoCleanupEnabled,
|
|
177
|
+
interval: CONFIG.userProfileAutoCleanupInterval,
|
|
178
|
+
})) {
|
|
179
|
+
// In the background: a long run must not hold the learning pass, and a failure must not fail it.
|
|
180
|
+
void import("../../services/user-profile/auto-cleanup.js")
|
|
181
|
+
.then(({ startAutoProfileCleanup }) => startAutoProfileCleanup(userId))
|
|
182
|
+
.catch(() => { });
|
|
209
183
|
}
|
|
210
184
|
if (CONFIG.showUserProfileToasts) {
|
|
211
185
|
await ctx.client?.tui
|
|
@@ -3,6 +3,7 @@ import { analyzeProfile, } from "../../core/profile-analysis.js";
|
|
|
3
3
|
import { profileBackoff, recordProfileFailure } from "../../core/profile-backoff.js";
|
|
4
4
|
import { log } from "../../services/logger.js";
|
|
5
5
|
import { getTags } from "../../services/tags.js";
|
|
6
|
+
import { shouldStartAutoCleanup } from "../../services/user-profile/auto-cleanup-trigger.js";
|
|
6
7
|
function replyText(reply) {
|
|
7
8
|
return reply.content
|
|
8
9
|
.filter((block) => block.type === "text" && typeof block.text === "string")
|
|
@@ -37,6 +38,13 @@ export function createPiProfileAnalyzer(resolveModel, timeoutMs = 120000) {
|
|
|
37
38
|
},
|
|
38
39
|
};
|
|
39
40
|
}
|
|
41
|
+
/** The model Pi uses for profile work, for the automatic cleanup. */
|
|
42
|
+
function resolveCleanupModel(input) {
|
|
43
|
+
if (input.resolveProfileModel)
|
|
44
|
+
return input.resolveProfileModel();
|
|
45
|
+
const handle = input.resolveModel?.();
|
|
46
|
+
return handle ? adaptPiProfileModel(handle) : null;
|
|
47
|
+
}
|
|
40
48
|
/**
|
|
41
49
|
* Pi-side profile learning lifecycle: analyse a batch of settled user prompts
|
|
42
50
|
* through the model bridge and persist via the shared profile manager. Failures
|
|
@@ -95,6 +103,15 @@ export async function performPiProfileLearning(input) {
|
|
|
95
103
|
prompts: input.prompts.length,
|
|
96
104
|
hadExisting: Boolean(existing),
|
|
97
105
|
});
|
|
106
|
+
if (shouldStartAutoCleanup(existing?.totalPromptsAnalyzed ?? 0, input.prompts.length, {
|
|
107
|
+
enabled: CONFIG.userProfileAutoCleanupEnabled,
|
|
108
|
+
interval: CONFIG.userProfileAutoCleanupInterval,
|
|
109
|
+
})) {
|
|
110
|
+
// In the background: a long run must not hold the learning pass, and a failure must not fail it.
|
|
111
|
+
void import("../../services/user-profile/auto-cleanup.js")
|
|
112
|
+
.then(({ startAutoProfileCleanup }) => startAutoProfileCleanup(userId, { resolveModel: () => resolveCleanupModel(input) }))
|
|
113
|
+
.catch(() => { });
|
|
114
|
+
}
|
|
98
115
|
if (CONFIG.showUserProfileToasts && input.notify) {
|
|
99
116
|
await Promise.resolve(input.notify({
|
|
100
117
|
title: "User Profile Updated",
|
package/dist/config.d.ts
CHANGED
|
@@ -107,7 +107,7 @@ interface OmmsConfig {
|
|
|
107
107
|
injectOn?: "first" | "always";
|
|
108
108
|
};
|
|
109
109
|
}
|
|
110
|
-
export declare const CONFIG_TEMPLATE = "{\n // ============================================\n // omms (Opinionated Modular Memory System) Configuration\n // ============================================\n \n // Storage location for the vector database. Leave unset to use the\n // default ~/.omms/data (a legacy ~/.opencode-mem/data store is migrated\n // there automatically on first start; see docs/omms-migration.md).\n // \"storagePath\": \"~/.omms/data\",\n\n \"userEmailOverride\": \"\",\n \"userNameOverride\": \"\",\n \n // ============================================\n // Embedding Model (for similarity search)\n // ============================================\n // Local = Hugging Face / ONNX via @huggingface/transformers (not Apple MLX).\n // Remote = set embeddingApiUrl (OpenAI-compatible /embeddings); embeddingApiKey is optional.\n \n // Default: Nomic Embed v1 (768 dimensions, 8192 context, multilingual)\n \"embeddingModel\": \"Xenova/nomic-embed-text-v1\",\n\n // Opt-in Nomic task prefixes (search_document: / search_query:). After enabling,\n // re-index existing memories so store and query vectors stay aligned.\n // \"embeddingUseTaskPrefixes\": true,\n \n // Auto-detected dimensions (no need to set manually)\n // \"embeddingDimensions\": 768,\n \n // Other recommended local models:\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-base-en\", // 768 dims, English-only, 8192 context\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-small-en\", // 512 dims, faster, 8192 context\n // \"embeddingModel\": \"Xenova/all-MiniLM-L6-v2\", // 384 dims, very fast, 512 context\n // \"embeddingModel\": \"Xenova/all-mpnet-base-v2\", // 768 dims, good quality, 512 context\n \n // Optional: OpenAI-compatible API for embeddings (URL required; key only if the server needs one)\n // \"embeddingApiUrl\": \"https://api.openai.com/v1\",\n // \"embeddingApiKey\": \"env://OPENAI_API_KEY\", // or \"sk-...\" / \"file:///path/to/key\"\n // \"embeddingModel\": \"text-embedding-3-small\", // 1536 dims, auto-detected\n \n // ============================================\n // Web Server Settings\n // ============================================\n \n // Start a background import of past chats on each host's next start.\n \"autoBackfill\": true,\n // \"piBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // \"opencodeBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // Directory maps for history recorded in moved or deleted directories.\n // \"importPathMaps\": [{ \"from\": \"~/code/app-feat-x\", \"to\": \"~/code/app\" }],\n // Unresolved directories to hide on the Directory maps page. They stay unimported.\n // \"importIgnoredDirectories\": [\"/private/tmp/scratch\"],\n // Claude Code's folder, when it is not ~/.claude (like CLAUDE_CONFIG_DIR).\n // \"claudeConfigDir\": \"~/.claude-work\",\n\n // Register the web app to start when you log in.\n \"webServerAutoStart\": true,\n // Enable web UI for managing memories (accessible at http://localhost:4747)\n \"webServerEnabled\": true,\n \n // Port for web UI server\n \"webServerPort\": 4747,\n \n // Host address for web UI (use 127.0.0.1 for local only, 0.0.0.0 for network access)\n \"webServerHost\": \"127.0.0.1\",\n\n // Optional HTTP Basic Auth for the web UI. Accepts literal, env://, or file:// secrets.\n // \"webServerAuthPassword\": \"\",\n // \"webServerAuthUsername\": \"\",\n\n // Deprecated: imported once into the API token table, then no longer read.\n // Create API tokens on the Settings page (Keys and access) instead.\n // \"webServerApiToken\": \"env://OMMS_WEB_TOKEN\",\n \n // ============================================\n // Database Settings\n // ============================================\n \n // Maximum vectors per database shard (auto-creates new shard when limit reached)\n \"maxVectorsPerShard\": 50000,\n \n // Automatically delete old memories based on retention period\n \"autoCleanupEnabled\": true,\n \n // Days to keep memories before auto-cleanup (only if autoCleanupEnabled is true)\n \"autoCleanupRetentionDays\": 30,\n \n // Automatically detect and remove duplicate memories\n \"deduplicationEnabled\": true,\n \n // Similarity threshold (0-1) for detecting duplicates (higher = stricter)\n \"deduplicationSimilarityThreshold\": 0.90,\n \n // ============================================\n // Memory Scope Settings\n // ============================================\n\n // Default scope for memory list/search queries\n // \"project\" keeps queries within the current project, \"all-projects\" searches across all project shards\n \"memory\": {\n \"defaultScope\": \"project\"\n },\n\n // ============================================\n // Model for auto-capture and profile learning (same rule in OpenCode and Pi)\n // ============================================\n\n // Which model summarises your work, in this order:\n // 1. The host model below: opencodeProvider/opencodeModel in OpenCode,\n // piProvider/piModel in Pi. Set the model to \"inherit\" to follow\n // whatever model the session is using, or to \"external\" to send\n // every call to the external API below.\n // 2. If no host model is set: the external API (memoryModel/memoryApiUrl/\n // memoryApiKey further down).\n // 3. If neither is set: the session's own model.\n // If the host model fails and the external API is configured, the external\n // API is used instead.\n //\n // Host models go through OpenCode's or Pi's own sign-in (OAuth like Claude\n // Pro/Max, GitHub Copilot, ChatGPT, or your own key there), so no separate API\n // key is needed here.\n //\n // Examples (OpenCode names from 'opencode providers list'; Pi names from its model list):\n // \"opencodeProvider\": \"anthropic\", \"opencodeModel\": \"claude-haiku-4-5-20251001\"\n // \"opencodeProvider\": \"openai\", \"opencodeModel\": \"inherit\"\n // \"piProvider\": \"openai-codex\", \"piModel\": \"gpt-5.6-luna\"\n //\n // \"opencodeProvider\": \"anthropic\",\n // \"opencodeModel\": \"claude-haiku-4-5-20251001\",\n // \"piProvider\": \"openai-codex\",\n // \"piModel\": \"gpt-5.6-luna\",\n\n // ============================================\n // Auto-Capture Settings\n // ============================================\n \n // Auto-capture runs in the background without blocking your main session.\n // Note: Ollama may not support tool calling. Use OpenAI, Anthropic, or Groq for best results.\n \n \"autoCaptureEnabled\": true,\n \n // Provider type: \"openai-chat\" | \"openai-responses\" | \"anthropic\" | \"minimax\" | \"orcarouter\" | \"google-gemini\"\n // Note: \"openai-chat\" is a generic OpenAI API-compatible mode.\n // Any service that follows the OpenAI Chat Completions API can use it via custom \"memoryApiUrl\".\n \"memoryProvider\": \"openai-chat\",\n \n // External API (step 2 above, and the fallback when a host model fails).\n // Uncomment all 3 lines and replace memoryApiKey before use:\n // \"memoryModel\": \"gpt-4o-mini\",\n // \"memoryApiUrl\": \"https://api.openai.com/v1\",\n // \"memoryApiKey\": \"sk-...\",\n\n // API Key Formats:\n // Direct value: \"sk-...\"\n // From file: \"file://~/.config/litellm-key.txt\"\n // From env variable: \"env://LITELLM_API_KEY\"\n \n // Examples for different providers:\n // Any OpenAI-compatible endpoint can use the \"openai-chat\" provider pattern below.\n // Common examples: DeepSeek, Qwen (via Alibaba Cloud ModelStudio),\n // Zhipu GLM (BigModel platform), and Kimi (Moonshot AI platform).\n\n // OpenAI Chat Completion (default, backward compatible):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"gpt-4o-mini\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n\n // DeepSeek (OpenAI-compatible example):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"deepseek-chat\"\n // \"memoryApiUrl\": \"https://api.deepseek.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // OpenAI Responses API (recommended, with session support):\n // \"memoryProvider\": \"openai-responses\"\n // \"memoryModel\": \"gpt-4o\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // Anthropic (with session support):\n // \"memoryProvider\": \"anthropic\"\n // \"memoryModel\": \"claude-3-5-haiku-20241022\"\n // \"memoryApiUrl\": \"https://api.anthropic.com/v1\"\n // \"memoryApiKey\": \"sk-ant-...\"\n\n // MiniMax (Anthropic Messages-compatible endpoint, with session support):\n // \"memoryProvider\": \"minimax\"\n // \"memoryModel\": \"MiniMax-M3\"\n // \"memoryApiUrl\": \"https://api.minimax.io\" // global endpoint\n // \"memoryApiKey\": \"<MiniMax API key>\"\n // // China endpoint: \"memoryApiUrl\": \"https://api.minimaxi.com\"\n // // Optional adaptive thinking for MiniMax-M3:\n // \"memoryExtraParams\": { \"thinking\": { \"type\": \"adaptive\" } }\n\n // OrcaRouter (OpenAI-compatible gateway, namespaced model IDs, with session support):\n // \"memoryProvider\": \"orcarouter\"\n // \"memoryApiKey\": \"<OrcaRouter API key>\"\n // // memoryApiUrl and memoryModel are optional \u2014 they default to\n // // https://api.orcarouter.ai/v1 and \"orcarouter/auto\" (a routing alias).\n // // OrcaRouter rejects bare model names, so if you set memoryModel, use a\n // // namespaced ID such as \"openai/gpt-5.5\" or \"deepseek/deepseek-v4-flash\".\n // \"memoryModel\": \"openai/gpt-5.5\"\n\n // Groq (OpenAI-compatible, use openai-chat provider):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"llama-3.3-70b-versatile\"\n // \"memoryApiUrl\": \"https://api.groq.com/openai/v1\"\n // \"memoryApiKey\": \"gsk_...\"\n \n // Maximum iterations for multi-turn AI analysis (for openai-responses, anthropic, and minimax)\n \"autoCaptureMaxIterations\": 5,\n \n // Timeout per iteration in milliseconds (30 seconds default)\n \"autoCaptureIterationTimeout\": 30000,\n\n // Tries for one turn's capture, on Pi and OpenCode, 2 and 4 seconds apart. After the\n // last one fails, a turn the model could not reach goes to the capture retry queue.\n \"autoCaptureMaxRetries\": 3,\n\n // Maximum UTF-8 bytes for the auto-capture markdown context sent to the summary model.\n // Prevents HTTP 400 context overflows on models with ~131K token windows (e.g. Groq Llama).\n // Rough guide: tokens \u2248 bytes / 4 for mixed code/prose.\n \"autoCaptureMaxContextBytes\": 131072,\n \n // Days to keep AI session history before cleanup\n \"aiSessionRetentionDays\": 7,\n\n // Capture diagnostics: every capture attempt always writes one metadata line\n // (model, stop reason, sizes, outcome) to ~/.omms/omms.log, with no\n // conversation text. Set captureTrace to true to also write each attempt's\n // prompt and raw reply to ~/.omms/traces/ for debugging. Traces can contain\n // conversation content: <private> text and common API key formats are\n // redacted, files are readable only by you, and they are deleted after\n // captureTraceRetentionDays. Only this global file can turn tracing on.\n // \"captureTrace\": false,\n // \"captureTraceRetentionDays\": 7,\n // \"captureAttemptRetentionDays\": 30,\n\n // Capture retry queue: when a live capture fails because the model cannot be\n // reached (network error, timeout, HTTP 408, 429 or 5xx), OMMS keeps a copy of\n // the turn in ~/.omms/data and tries it again later. The copy has <private>\n // text and common API key formats removed. Turns are deleted when the retry\n // ends or after captureRetryRetentionHours (0 to 720). 0 turns the queue off.\n // Only this global file can set it.\n // \"captureRetryRetentionHours\": 72,\n\n // Temperature for AI API requests (set to false to omit parameter for models that don't support it)\n // Some reasoning models (like o1, o3, gpt-5) don't support temperature parameter\n // Set to false and add \"memoryTemperature\": false in config when using such models\n \"memoryTemperature\": 0.3,\n\n // Extra parameters to include in API request body\n // Useful for local inference servers (e.g. llama-server with --jinja) that support\n // additional parameters like disabling thinking/reasoning mode\n // Example for Qwen3 models: { \"enable_thinking\": false }\n // \"memoryExtraParams\": {},\n\n // Language for auto-capture summaries (default: \"auto\" for auto-detection)\n // Options: \"auto\", \"en\", \"id\", \"zh\", \"ja\", \"es\", \"fr\", \"de\", \"ru\", \"pt\", \"ar\", \"ko\"\n // \"autoCaptureLanguage\": \"auto\",\n\n // ============================================\n // Toast Notifications\n // ============================================\n\n // Show toast when memory is auto-captured\n \"showAutoCaptureToasts\": true,\n\n // Show toast when user profile is updated\n \"showUserProfileToasts\": true,\n\n // Show toast for error messages\n \"showErrorToasts\": true,\n\n // ============================================\n // User Profile System\n // ============================================\n\n // Analyze user prompts every N prompts to build/update your user profile\n // When N uncaptured prompts accumulate, AI will analyze them to identify:\n // - User preferences (code style, communication style, tool preferences)\n // - User patterns (recurring topics, problem domains, technical interests)\n // - User workflows (development habits, sequences, learning style)\n // - Skill level (overall and per-domain assessment)\n \"userProfileAnalysisInterval\": 10,\n\n // Days before inactive items (all types) are eligible for removal\n \"userProfileStaleDays\": 2,\n\n // Days before an inactive workflow is eligible for removal. A workflow seen\n // userProfileMinEvidenceForRetention times or more is kept.\n \"userProfileWorkflowStaleDays\": 30,\n\n // Number of preferences shown in UI\n \"userProfileDisplayPreferences\": 20,\n \n // Number of patterns shown in UI\n \"userProfileDisplayPatterns\": 15,\n \n // Number of workflows shown in UI\n \"userProfileDisplayWorkflows\": 10,\n \n // Number of preferences injected into LLM conversation context\n // Keep this small \u2014 the strongest signals are enough; more dilute LLM attention\n \"userProfileInjectPreferences\": 5,\n \n // Number of patterns injected into LLM conversation context\n \"userProfileInjectPatterns\": 5,\n \n // Number of workflows injected into LLM conversation context\n \"userProfileInjectWorkflows\": 3,\n \n // Days before preference confidence starts to decay (if not reinforced)\n // Preferences that aren't seen again will gradually lose confidence and be removed\n \"userProfileConfidenceDecayDays\": 30,\n \n // Number of profile versions to keep in changelog (for rollback/debugging)\n // Older versions are automatically cleaned up\n \"userProfileChangelogRetentionCount\": 5,\n\n // Minimum evidence count for a preference/pattern to survive confidence decay\n // Items confirmed fewer times are more likely to be pruned when confidence decays\n \"userProfileMinEvidenceForRetention\": 3,\n\n // Periodically merge duplicate or irrelevant profile items with the configured AI provider\n \"userProfileAutoCleanupEnabled\": true,\n // Number of analyzed user prompts between automatic AI cleanup runs\n \"userProfileAutoCleanupInterval\": 100,\n\n // Enable LLM validation of existing preferences against recent behavior.\n // When enabled, each analysis round checks if top-5 preferences still match recent prompts.\n // Experimental \u2014 disabled by default.\n \"userProfileValidationEnabled\": false,\n\n // ============================================\n // Search Settings\n // ============================================\n \n // Minimum similarity score (0-1) for memory search results\n \"similarityThreshold\": 0.6,\n\n // Maximum number of memories to return in search results.\n // Manual searches can ask for fewer; prompt retrieval uses this ceiling.\n \"maxMemories\": 10,\n\n // ============================================\n // Memory Context Controls\n // ============================================\n\n // Recent memories added at fresh session start (OpenCode V1 and Claude Code).\n // Pi and OpenCode V2 search the store with each prompt instead.\n \"chatMessage\": {\n \"enabled\": true,\n // Recent memories injected at session start.\n \"maxMemories\": 3,\n \"excludeCurrentSession\": true,\n // When to inject: \"first\" message of a session or \"always\".\n // \"injectOn\": \"first\",\n },\n\n // OpenCode profile-learning input limit, in UTF-8 bytes. Smaller values can\n // omit prompts from that input. Does not limit Pi or Claude Code profile input.\n \"userProfileMaxContextBytes\": 32768,\n\n // Shared budget for automatic memory context (profile text, memories, and\n // formatting) across all hosts, in approximate tokens: ceil(UTF-8 bytes / 4).\n // Covers prompt retrieval, recent-memory injection, and session-memory\n // restoration. Not an exact provider token limit or a spending limit.\n \"retrievalMaxTokens\": 2000,\n\n // ============================================\n // Advanced Settings\n // ============================================\n \n // Inject user profile into AI context (preferences, patterns, workflows)\n \"injectProfile\": true\n}\n";
|
|
110
|
+
export declare const CONFIG_TEMPLATE = "{\n // ============================================\n // omms (Opinionated Modular Memory System) Configuration\n // ============================================\n \n // Storage location for the vector database. Leave unset to use the\n // default ~/.omms/data (a legacy ~/.opencode-mem/data store is migrated\n // there automatically on first start; see docs/omms-migration.md).\n // \"storagePath\": \"~/.omms/data\",\n\n \"userEmailOverride\": \"\",\n \"userNameOverride\": \"\",\n \n // ============================================\n // Embedding Model (for similarity search)\n // ============================================\n // Local = Hugging Face / ONNX via @huggingface/transformers (not Apple MLX).\n // Remote = set embeddingApiUrl (OpenAI-compatible /embeddings); embeddingApiKey is optional.\n \n // Default: Nomic Embed v1 (768 dimensions, 8192 context, multilingual)\n \"embeddingModel\": \"Xenova/nomic-embed-text-v1\",\n\n // Opt-in Nomic task prefixes (search_document: / search_query:). After enabling,\n // re-index existing memories so store and query vectors stay aligned.\n // \"embeddingUseTaskPrefixes\": true,\n \n // Auto-detected dimensions (no need to set manually)\n // \"embeddingDimensions\": 768,\n \n // Other recommended local models:\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-base-en\", // 768 dims, English-only, 8192 context\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-small-en\", // 512 dims, faster, 8192 context\n // \"embeddingModel\": \"Xenova/all-MiniLM-L6-v2\", // 384 dims, very fast, 512 context\n // \"embeddingModel\": \"Xenova/all-mpnet-base-v2\", // 768 dims, good quality, 512 context\n \n // Optional: OpenAI-compatible API for embeddings (URL required; key only if the server needs one)\n // \"embeddingApiUrl\": \"https://api.openai.com/v1\",\n // \"embeddingApiKey\": \"env://OPENAI_API_KEY\", // or \"sk-...\" / \"file:///path/to/key\"\n // \"embeddingModel\": \"text-embedding-3-small\", // 1536 dims, auto-detected\n \n // ============================================\n // Web Server Settings\n // ============================================\n \n // Start a background import of past chats on each host's next start.\n \"autoBackfill\": true,\n // \"piBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // \"opencodeBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // Directory maps for history recorded in moved or deleted directories.\n // \"importPathMaps\": [{ \"from\": \"~/code/app-feat-x\", \"to\": \"~/code/app\" }],\n // Unresolved directories to hide on the Directory maps page. They stay unimported.\n // \"importIgnoredDirectories\": [\"/private/tmp/scratch\"],\n // Claude Code's folder, when it is not ~/.claude (like CLAUDE_CONFIG_DIR).\n // \"claudeConfigDir\": \"~/.claude-work\",\n\n // Register the web app to start when you log in.\n \"webServerAutoStart\": true,\n // Enable web UI for managing memories (accessible at http://localhost:4747)\n \"webServerEnabled\": true,\n \n // Port for web UI server\n \"webServerPort\": 4747,\n \n // Host address for web UI (use 127.0.0.1 for local only, 0.0.0.0 for network access)\n \"webServerHost\": \"127.0.0.1\",\n\n // Optional HTTP Basic Auth for the web UI. Accepts literal, env://, or file:// secrets.\n // \"webServerAuthPassword\": \"\",\n // \"webServerAuthUsername\": \"\",\n\n // Deprecated: imported once into the API token table, then no longer read.\n // Create API tokens on the Settings page (Keys and access) instead.\n // \"webServerApiToken\": \"env://OMMS_WEB_TOKEN\",\n \n // ============================================\n // Database Settings\n // ============================================\n \n // Maximum vectors per database shard (auto-creates new shard when limit reached)\n \"maxVectorsPerShard\": 50000,\n \n // Automatically delete old memories based on retention period\n \"autoCleanupEnabled\": true,\n \n // Days to keep memories before auto-cleanup (only if autoCleanupEnabled is true)\n \"autoCleanupRetentionDays\": 30,\n \n // Automatically detect and remove duplicate memories\n \"deduplicationEnabled\": true,\n \n // Similarity threshold (0-1) for detecting duplicates (higher = stricter)\n \"deduplicationSimilarityThreshold\": 0.90,\n \n // ============================================\n // Memory Scope Settings\n // ============================================\n\n // Default scope for memory list/search queries\n // \"project\" keeps queries within the current project, \"all-projects\" searches across all project shards\n \"memory\": {\n \"defaultScope\": \"project\"\n },\n\n // ============================================\n // Model for auto-capture and profile learning (same rule in OpenCode and Pi)\n // ============================================\n\n // Which model summarises your work, in this order:\n // 1. The host model below: opencodeProvider/opencodeModel in OpenCode,\n // piProvider/piModel in Pi. Set the model to \"inherit\" to follow\n // whatever model the session is using, or to \"external\" to send\n // every call to the external API below.\n // 2. If no host model is set: the external API (memoryModel/memoryApiUrl/\n // memoryApiKey further down).\n // 3. If neither is set: the session's own model.\n // If the host model fails and the external API is configured, the external\n // API is used instead.\n //\n // Host models go through OpenCode's or Pi's own sign-in (OAuth like Claude\n // Pro/Max, GitHub Copilot, ChatGPT, or your own key there), so no separate API\n // key is needed here.\n //\n // Examples (OpenCode names from 'opencode providers list'; Pi names from its model list):\n // \"opencodeProvider\": \"anthropic\", \"opencodeModel\": \"claude-haiku-4-5-20251001\"\n // \"opencodeProvider\": \"openai\", \"opencodeModel\": \"inherit\"\n // \"piProvider\": \"openai-codex\", \"piModel\": \"gpt-5.6-luna\"\n //\n // \"opencodeProvider\": \"anthropic\",\n // \"opencodeModel\": \"claude-haiku-4-5-20251001\",\n // \"piProvider\": \"openai-codex\",\n // \"piModel\": \"gpt-5.6-luna\",\n\n // ============================================\n // Auto-Capture Settings\n // ============================================\n \n // Auto-capture runs in the background without blocking your main session.\n // Note: Ollama may not support tool calling. Use OpenAI, Anthropic, or Groq for best results.\n \n \"autoCaptureEnabled\": true,\n \n // Provider type: \"openai-chat\" | \"openai-responses\" | \"anthropic\" | \"minimax\" | \"orcarouter\" | \"google-gemini\"\n // Note: \"openai-chat\" is a generic OpenAI API-compatible mode.\n // Any service that follows the OpenAI Chat Completions API can use it via custom \"memoryApiUrl\".\n \"memoryProvider\": \"openai-chat\",\n \n // External API (step 2 above, and the fallback when a host model fails).\n // Uncomment all 3 lines and replace memoryApiKey before use:\n // \"memoryModel\": \"gpt-4o-mini\",\n // \"memoryApiUrl\": \"https://api.openai.com/v1\",\n // \"memoryApiKey\": \"sk-...\",\n\n // API Key Formats:\n // Direct value: \"sk-...\"\n // From file: \"file://~/.config/litellm-key.txt\"\n // From env variable: \"env://LITELLM_API_KEY\"\n \n // Examples for different providers:\n // Any OpenAI-compatible endpoint can use the \"openai-chat\" provider pattern below.\n // Common examples: DeepSeek, Qwen (via Alibaba Cloud ModelStudio),\n // Zhipu GLM (BigModel platform), and Kimi (Moonshot AI platform).\n\n // OpenAI Chat Completion (default, backward compatible):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"gpt-4o-mini\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n\n // DeepSeek (OpenAI-compatible example):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"deepseek-chat\"\n // \"memoryApiUrl\": \"https://api.deepseek.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // OpenAI Responses API (recommended, with session support):\n // \"memoryProvider\": \"openai-responses\"\n // \"memoryModel\": \"gpt-4o\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // Anthropic (with session support):\n // \"memoryProvider\": \"anthropic\"\n // \"memoryModel\": \"claude-3-5-haiku-20241022\"\n // \"memoryApiUrl\": \"https://api.anthropic.com/v1\"\n // \"memoryApiKey\": \"sk-ant-...\"\n\n // MiniMax (Anthropic Messages-compatible endpoint, with session support):\n // \"memoryProvider\": \"minimax\"\n // \"memoryModel\": \"MiniMax-M3\"\n // \"memoryApiUrl\": \"https://api.minimax.io\" // global endpoint\n // \"memoryApiKey\": \"<MiniMax API key>\"\n // // China endpoint: \"memoryApiUrl\": \"https://api.minimaxi.com\"\n // // Optional adaptive thinking for MiniMax-M3:\n // \"memoryExtraParams\": { \"thinking\": { \"type\": \"adaptive\" } }\n\n // OrcaRouter (OpenAI-compatible gateway, namespaced model IDs, with session support):\n // \"memoryProvider\": \"orcarouter\"\n // \"memoryApiKey\": \"<OrcaRouter API key>\"\n // // memoryApiUrl and memoryModel are optional \u2014 they default to\n // // https://api.orcarouter.ai/v1 and \"orcarouter/auto\" (a routing alias).\n // // OrcaRouter rejects bare model names, so if you set memoryModel, use a\n // // namespaced ID such as \"openai/gpt-5.5\" or \"deepseek/deepseek-v4-flash\".\n // \"memoryModel\": \"openai/gpt-5.5\"\n\n // Groq (OpenAI-compatible, use openai-chat provider):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"llama-3.3-70b-versatile\"\n // \"memoryApiUrl\": \"https://api.groq.com/openai/v1\"\n // \"memoryApiKey\": \"gsk_...\"\n \n // Maximum iterations for multi-turn AI analysis (for openai-responses, anthropic, and minimax)\n \"autoCaptureMaxIterations\": 5,\n \n // Timeout per iteration in milliseconds (30 seconds default)\n \"autoCaptureIterationTimeout\": 30000,\n\n // Tries for one turn's capture, on Pi and OpenCode, 2 and 4 seconds apart. After the\n // last one fails, a turn the model could not reach goes to the capture retry queue.\n \"autoCaptureMaxRetries\": 3,\n\n // Maximum UTF-8 bytes for the auto-capture markdown context sent to the summary model.\n // Prevents HTTP 400 context overflows on models with ~131K token windows (e.g. Groq Llama).\n // Rough guide: tokens \u2248 bytes / 4 for mixed code/prose.\n \"autoCaptureMaxContextBytes\": 131072,\n \n // Days to keep AI session history before cleanup\n \"aiSessionRetentionDays\": 7,\n\n // Capture diagnostics: every capture attempt always writes one metadata line\n // (model, stop reason, sizes, outcome) to ~/.omms/omms.log, with no\n // conversation text. Set captureTrace to true to also write each attempt's\n // prompt and raw reply to ~/.omms/traces/ for debugging. Traces can contain\n // conversation content: <private> text and common API key formats are\n // redacted, files are readable only by you, and they are deleted after\n // captureTraceRetentionDays. Only this global file can turn tracing on.\n // \"captureTrace\": false,\n // \"captureTraceRetentionDays\": 7,\n // \"captureAttemptRetentionDays\": 30,\n\n // Capture retry queue: when a live capture fails because the model cannot be\n // reached (network error, timeout, HTTP 408, 429 or 5xx), OMMS keeps a copy of\n // the turn in ~/.omms/data and tries it again later. The copy has <private>\n // text and common API key formats removed. Turns are deleted when the retry\n // ends or after captureRetryRetentionHours (0 to 720). 0 turns the queue off.\n // Only this global file can set it.\n // \"captureRetryRetentionHours\": 72,\n\n // Temperature for AI API requests (set to false to omit parameter for models that don't support it)\n // Some reasoning models (like o1, o3, gpt-5) don't support temperature parameter\n // Set to false and add \"memoryTemperature\": false in config when using such models\n \"memoryTemperature\": 0.3,\n\n // Extra parameters to include in API request body\n // Useful for local inference servers (e.g. llama-server with --jinja) that support\n // additional parameters like disabling thinking/reasoning mode\n // Example for Qwen3 models: { \"enable_thinking\": false }\n // \"memoryExtraParams\": {},\n\n // Language for auto-capture summaries (default: \"auto\" for auto-detection)\n // Options: \"auto\", \"en\", \"id\", \"zh\", \"ja\", \"es\", \"fr\", \"de\", \"ru\", \"pt\", \"ar\", \"ko\"\n // \"autoCaptureLanguage\": \"auto\",\n\n // ============================================\n // Toast Notifications\n // ============================================\n\n // Show toast when memory is auto-captured\n \"showAutoCaptureToasts\": true,\n\n // Show toast when user profile is updated\n \"showUserProfileToasts\": true,\n\n // Show toast for error messages\n \"showErrorToasts\": true,\n\n // ============================================\n // User Profile System\n // ============================================\n\n // Analyze user prompts every N prompts to build/update your user profile\n // When N uncaptured prompts accumulate, AI will analyze them to identify:\n // - User preferences (code style, communication style, tool preferences)\n // - User patterns (recurring topics, problem domains, technical interests)\n // - User workflows (development habits, sequences, learning style)\n // - Skill level (overall and per-domain assessment)\n \"userProfileAnalysisInterval\": 10,\n\n // Days before inactive items (all types) are eligible for removal\n \"userProfileStaleDays\": 2,\n\n // Days before an inactive workflow is eligible for removal. A workflow seen\n // userProfileMinEvidenceForRetention times or more is kept.\n \"userProfileWorkflowStaleDays\": 30,\n\n // Number of preferences shown in UI\n \"userProfileDisplayPreferences\": 20,\n \n // Number of patterns shown in UI\n \"userProfileDisplayPatterns\": 15,\n \n // Number of workflows shown in UI\n \"userProfileDisplayWorkflows\": 10,\n \n // Number of preferences injected into LLM conversation context\n // Keep this small \u2014 the strongest signals are enough; more dilute LLM attention\n \"userProfileInjectPreferences\": 5,\n \n // Number of patterns injected into LLM conversation context\n \"userProfileInjectPatterns\": 5,\n \n // Number of workflows injected into LLM conversation context\n \"userProfileInjectWorkflows\": 3,\n \n // Days before preference confidence starts to decay (if not reinforced)\n // Preferences that aren't seen again will gradually lose confidence and be removed\n \"userProfileConfidenceDecayDays\": 30,\n \n // Number of profile versions to keep in changelog (for rollback/debugging)\n // Older versions are automatically cleaned up\n \"userProfileChangelogRetentionCount\": 5,\n\n // Minimum evidence count for a preference/pattern to survive confidence decay\n // Items confirmed fewer times are more likely to be pruned when confidence decays\n \"userProfileMinEvidenceForRetention\": 3,\n\n // Merge duplicate or vague profile items every N analysed prompts (about 1 model call per 100 items)\n \"userProfileAutoCleanupEnabled\": true,\n \"userProfileAutoCleanupInterval\": 500,\n\n // Enable LLM validation of existing preferences against recent behavior.\n // When enabled, each analysis round checks if top-5 preferences still match recent prompts.\n // Experimental \u2014 disabled by default.\n \"userProfileValidationEnabled\": false,\n\n // ============================================\n // Search Settings\n // ============================================\n \n // Minimum similarity score (0-1) for memory search results\n \"similarityThreshold\": 0.6,\n\n // Maximum number of memories to return in search results.\n // Manual searches can ask for fewer; prompt retrieval uses this ceiling.\n \"maxMemories\": 10,\n\n // ============================================\n // Memory Context Controls\n // ============================================\n\n // Recent memories added at fresh session start (OpenCode V1 and Claude Code).\n // Pi and OpenCode V2 search the store with each prompt instead.\n \"chatMessage\": {\n \"enabled\": true,\n // Recent memories injected at session start.\n \"maxMemories\": 3,\n \"excludeCurrentSession\": true,\n // When to inject: \"first\" message of a session or \"always\".\n // \"injectOn\": \"first\",\n },\n\n // OpenCode profile-learning input limit, in UTF-8 bytes. Smaller values can\n // omit prompts from that input. Does not limit Pi or Claude Code profile input.\n \"userProfileMaxContextBytes\": 32768,\n\n // Shared budget for automatic memory context (profile text, memories, and\n // formatting) across all hosts, in approximate tokens: ceil(UTF-8 bytes / 4).\n // Covers prompt retrieval, recent-memory injection, and session-memory\n // restoration. Not an exact provider token limit or a spending limit.\n \"retrievalMaxTokens\": 2000,\n\n // ============================================\n // Advanced Settings\n // ============================================\n \n // Inject user profile into AI context (preferences, patterns, workflows)\n \"injectProfile\": true\n}\n";
|
|
111
111
|
export declare function normalizeAutoCaptureMaxContextBytes(value: number): number;
|
|
112
112
|
/** Whole hours from 0 to 720. A value that is not a number gives the default. */
|
|
113
113
|
export declare function normalizeCaptureRetryRetentionHours(value: unknown): number;
|
package/dist/config.js
CHANGED
|
@@ -92,7 +92,7 @@ const DEFAULTS = {
|
|
|
92
92
|
userProfileEmbeddingMinDescriptionLength: 5,
|
|
93
93
|
userProfileMinEvidenceForRetention: 3,
|
|
94
94
|
userProfileAutoCleanupEnabled: true,
|
|
95
|
-
userProfileAutoCleanupInterval:
|
|
95
|
+
userProfileAutoCleanupInterval: 500,
|
|
96
96
|
userProfileValidationEnabled: false,
|
|
97
97
|
showAutoCaptureToasts: true,
|
|
98
98
|
showUserProfileToasts: true,
|
|
@@ -486,10 +486,9 @@ export const CONFIG_TEMPLATE = `{
|
|
|
486
486
|
// Items confirmed fewer times are more likely to be pruned when confidence decays
|
|
487
487
|
"userProfileMinEvidenceForRetention": 3,
|
|
488
488
|
|
|
489
|
-
//
|
|
489
|
+
// Merge duplicate or vague profile items every N analysed prompts (about 1 model call per 100 items)
|
|
490
490
|
"userProfileAutoCleanupEnabled": true,
|
|
491
|
-
|
|
492
|
-
"userProfileAutoCleanupInterval": 100,
|
|
491
|
+
"userProfileAutoCleanupInterval": 500,
|
|
493
492
|
|
|
494
493
|
// Enable LLM validation of existing preferences against recent behavior.
|
|
495
494
|
// When enabled, each analysis round checks if top-5 preferences still match recent prompts.
|
|
@@ -872,6 +871,8 @@ export function initConfig(directory, options = {}) {
|
|
|
872
871
|
const projectOverrides = { ...projectConfig };
|
|
873
872
|
delete projectOverrides.autoCleanupEnabled;
|
|
874
873
|
delete projectOverrides.autoCleanupRetentionDays;
|
|
874
|
+
delete projectOverrides.userProfileAutoCleanupEnabled;
|
|
875
|
+
delete projectOverrides.userProfileAutoCleanupInterval;
|
|
875
876
|
// A checked-in project file must not start recording conversations; it may
|
|
876
877
|
// only turn tracing off for its project.
|
|
877
878
|
if (projectOverrides.captureTrace === true) {
|
|
@@ -14,6 +14,7 @@ import { log } from "../services/logger.js";
|
|
|
14
14
|
import { isFullyPrivate, stripPrivateContent } from "../services/privacy.js";
|
|
15
15
|
import { getTags } from "../services/tags.js";
|
|
16
16
|
import { extractClaudeWindowsAfter, parseClaudeTranscript, } from "./claude-conversation.js";
|
|
17
|
+
import { learnClaudeProfile } from "./claude-profile-learning.js";
|
|
17
18
|
import { defaultClaudeProjectsRoot } from "./claude-reader.js";
|
|
18
19
|
// The web app's side of the Claude Code hooks (design decisions 4, 6, 7):
|
|
19
20
|
// retrieval for SessionStart and UserPromptSubmit, a one-worker capture queue
|
|
@@ -388,8 +389,9 @@ async function captureTurn(request, deps) {
|
|
|
388
389
|
// The model answers again, so earlier failed turns may go through now.
|
|
389
390
|
startCaptureRetryDrain(HOST);
|
|
390
391
|
}
|
|
391
|
-
if (counts.captured + counts.skipped > 0)
|
|
392
|
-
await
|
|
392
|
+
if (counts.captured + counts.skipped > 0) {
|
|
393
|
+
await learnClaudeProfile(request.cwd, async () => deps.profileModel?.() ?? (await externalModels()).profile);
|
|
394
|
+
}
|
|
393
395
|
}
|
|
394
396
|
// --- Profile learning ---
|
|
395
397
|
async function recordPrompt(request, unit, memoryId) {
|
|
@@ -407,70 +409,6 @@ async function recordPrompt(request, unit, memoryId) {
|
|
|
407
409
|
});
|
|
408
410
|
}
|
|
409
411
|
}
|
|
410
|
-
let learning = false;
|
|
411
|
-
/**
|
|
412
|
-
* Build or update the user profile from the recorded prompts once there are
|
|
413
|
-
* `userProfileAnalysisInterval` of them. A failure is logged and never
|
|
414
|
-
* blocks capture; the prompts stay for the next pass.
|
|
415
|
-
*/
|
|
416
|
-
async function learnProfile(directory, deps) {
|
|
417
|
-
const interval = CONFIG.userProfileAnalysisInterval ?? 0;
|
|
418
|
-
if (!(interval > 0) || learning)
|
|
419
|
-
return;
|
|
420
|
-
const { profileBackoff, profileCatchUpLock, recordProfileFailure } = await import("../core/profile-backoff.js");
|
|
421
|
-
// A catch-up run, here or in a terminal, analyses the same prompts; skip the live pass meanwhile.
|
|
422
|
-
if (!profileBackoff.canRun() || profileCatchUpLock.isActive())
|
|
423
|
-
return;
|
|
424
|
-
const { CatchUpLease } = await import("../services/user-prompt/profile-catch-up-lease.js");
|
|
425
|
-
if (await new CatchUpLease().isActive().catch(() => false))
|
|
426
|
-
return;
|
|
427
|
-
learning = true;
|
|
428
|
-
try {
|
|
429
|
-
const [{ userPromptManager }, { userProfileManager }, { analyzeProfile }] = await Promise.all([
|
|
430
|
-
import("../services/user-prompt/user-prompt-manager.js"),
|
|
431
|
-
import("../services/user-profile/user-profile-manager.js"),
|
|
432
|
-
import("../core/profile-analysis.js"),
|
|
433
|
-
]);
|
|
434
|
-
await userPromptManager.skipTrivialPromptsForLearning();
|
|
435
|
-
if ((await userPromptManager.countUnanalyzedForUserLearning()) < interval)
|
|
436
|
-
return;
|
|
437
|
-
const user = getTags(directory).user;
|
|
438
|
-
if (!user.userEmail) {
|
|
439
|
-
log("Claude Code profile learning skipped", { code: "no-user-email" });
|
|
440
|
-
return;
|
|
441
|
-
}
|
|
442
|
-
const batch = await userPromptManager.getPromptsForUserLearning(interval, {
|
|
443
|
-
recentFirst: true,
|
|
444
|
-
});
|
|
445
|
-
if (batch.length === 0)
|
|
446
|
-
return;
|
|
447
|
-
const model = deps.profileModel?.() ?? (await externalModels()).profile;
|
|
448
|
-
const existing = await userProfileManager.getActiveProfile(user.userEmail);
|
|
449
|
-
const context = batch.map((prompt, index) => `${index + 1}. ${prompt.content}`).join("\n");
|
|
450
|
-
const analysis = await analyzeProfile(model, context, existing ? { id: existing.id, profileData: existing.profileData } : null);
|
|
451
|
-
if (existing) {
|
|
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
|
-
if (!updated)
|
|
455
|
-
throw new Error("Profile update conflict");
|
|
456
|
-
}
|
|
457
|
-
else {
|
|
458
|
-
await userProfileManager.createProfile(user.userEmail, user.displayName || user.userEmail, user.userName || user.userEmail, user.userEmail, analysis.raw, batch.length);
|
|
459
|
-
}
|
|
460
|
-
await userPromptManager.markMultipleAsUserLearningCaptured(batch.map((prompt) => prompt.id));
|
|
461
|
-
profileBackoff.recordSuccess();
|
|
462
|
-
log("Claude Code profile learning finished", {
|
|
463
|
-
prompts: batch.length,
|
|
464
|
-
hadExisting: Boolean(existing),
|
|
465
|
-
});
|
|
466
|
-
}
|
|
467
|
-
catch (error) {
|
|
468
|
-
recordProfileFailure("claude-code", "Claude Code profile learning failed", error);
|
|
469
|
-
}
|
|
470
|
-
finally {
|
|
471
|
-
learning = false;
|
|
472
|
-
}
|
|
473
|
-
}
|
|
474
412
|
// --- Web app start ---
|
|
475
413
|
/**
|
|
476
414
|
* Register the Claude Code retry drain in this web app and start one retry
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ModelPort } from "../core/profile-analysis.js";
|
|
2
|
+
/**
|
|
3
|
+
* Build or update the user profile from the recorded prompts once there are
|
|
4
|
+
* `userProfileAnalysisInterval` of them. A failure is logged and never
|
|
5
|
+
* blocks capture; the prompts stay for the next pass.
|
|
6
|
+
*/
|
|
7
|
+
export declare function learnClaudeProfile(directory: string, resolveModel: () => ModelPort | Promise<ModelPort>): Promise<void>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { CONFIG } from "../config.js";
|
|
2
|
+
import { log } from "../services/logger.js";
|
|
3
|
+
import { getTags } from "../services/tags.js";
|
|
4
|
+
import { shouldStartAutoCleanup } from "../services/user-profile/auto-cleanup-trigger.js";
|
|
5
|
+
let learning = false;
|
|
6
|
+
/**
|
|
7
|
+
* Build or update the user profile from the recorded prompts once there are
|
|
8
|
+
* `userProfileAnalysisInterval` of them. A failure is logged and never
|
|
9
|
+
* blocks capture; the prompts stay for the next pass.
|
|
10
|
+
*/
|
|
11
|
+
export async function learnClaudeProfile(directory, resolveModel) {
|
|
12
|
+
const interval = CONFIG.userProfileAnalysisInterval ?? 0;
|
|
13
|
+
if (!(interval > 0) || learning)
|
|
14
|
+
return;
|
|
15
|
+
const { profileBackoff, profileCatchUpLock, recordProfileFailure } = await import("../core/profile-backoff.js");
|
|
16
|
+
// A catch-up run, here or in a terminal, analyses the same prompts; skip the live pass meanwhile.
|
|
17
|
+
if (!profileBackoff.canRun() || profileCatchUpLock.isActive())
|
|
18
|
+
return;
|
|
19
|
+
const { CatchUpLease } = await import("../services/user-prompt/profile-catch-up-lease.js");
|
|
20
|
+
if (await new CatchUpLease().isActive().catch(() => false))
|
|
21
|
+
return;
|
|
22
|
+
learning = true;
|
|
23
|
+
try {
|
|
24
|
+
const [{ userPromptManager }, { userProfileManager }, { analyzeProfile }] = await Promise.all([
|
|
25
|
+
import("../services/user-prompt/user-prompt-manager.js"),
|
|
26
|
+
import("../services/user-profile/user-profile-manager.js"),
|
|
27
|
+
import("../core/profile-analysis.js"),
|
|
28
|
+
]);
|
|
29
|
+
await userPromptManager.skipTrivialPromptsForLearning();
|
|
30
|
+
if ((await userPromptManager.countUnanalyzedForUserLearning()) < interval)
|
|
31
|
+
return;
|
|
32
|
+
const user = getTags(directory).user;
|
|
33
|
+
if (!user.userEmail) {
|
|
34
|
+
log("Claude Code profile learning skipped", { code: "no-user-email" });
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
const batch = await userPromptManager.getPromptsForUserLearning(interval, {
|
|
38
|
+
recentFirst: true,
|
|
39
|
+
});
|
|
40
|
+
if (batch.length === 0)
|
|
41
|
+
return;
|
|
42
|
+
const model = await resolveModel();
|
|
43
|
+
const existing = await userProfileManager.getActiveProfile(user.userEmail);
|
|
44
|
+
const context = batch.map((prompt, index) => `${index + 1}. ${prompt.content}`).join("\n");
|
|
45
|
+
const analysis = await analyzeProfile(model, context, existing ? { id: existing.id, profileData: existing.profileData } : null);
|
|
46
|
+
if (existing) {
|
|
47
|
+
const { saveMergedProfile } = await import("../services/user-profile/merge-and-save.js");
|
|
48
|
+
const updated = await saveMergedProfile(userProfileManager, existing, analysis, batch.length, `Claude Code analysis of ${batch.length} prompts`);
|
|
49
|
+
if (!updated)
|
|
50
|
+
throw new Error("Profile update conflict");
|
|
51
|
+
}
|
|
52
|
+
else {
|
|
53
|
+
await userProfileManager.createProfile(user.userEmail, user.displayName || user.userEmail, user.userName || user.userEmail, user.userEmail, analysis.raw, batch.length);
|
|
54
|
+
}
|
|
55
|
+
await userPromptManager.markMultipleAsUserLearningCaptured(batch.map((prompt) => prompt.id));
|
|
56
|
+
profileBackoff.recordSuccess();
|
|
57
|
+
log("Claude Code profile learning finished", {
|
|
58
|
+
prompts: batch.length,
|
|
59
|
+
hadExisting: Boolean(existing),
|
|
60
|
+
});
|
|
61
|
+
if (shouldStartAutoCleanup(existing?.totalPromptsAnalyzed ?? 0, batch.length, {
|
|
62
|
+
enabled: CONFIG.userProfileAutoCleanupEnabled,
|
|
63
|
+
interval: CONFIG.userProfileAutoCleanupInterval,
|
|
64
|
+
})) {
|
|
65
|
+
// In the background, with the model learning used: a long run must not hold the pass.
|
|
66
|
+
const userId = user.userEmail;
|
|
67
|
+
void import("../services/user-profile/auto-cleanup.js")
|
|
68
|
+
.then(({ startAutoProfileCleanup }) => startAutoProfileCleanup(userId, { resolveModel: () => model }))
|
|
69
|
+
.catch(() => { });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
recordProfileFailure("claude-code", "Claude Code profile learning failed", error);
|
|
74
|
+
}
|
|
75
|
+
finally {
|
|
76
|
+
learning = false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -95,7 +95,7 @@ export async function handleAiCleanupApply(userId, body, deps = {}) {
|
|
|
95
95
|
try {
|
|
96
96
|
const { getAiCleanupResult, stopAiCleanupJob } = await import("./user-profile/ai-cleanup-job.js");
|
|
97
97
|
const { applyCleanupEdits } = await import("./user-profile/ai-cleanup-apply.js");
|
|
98
|
-
const {
|
|
98
|
+
const { applyToNewestProfile } = await import("./user-profile/profile-edit-save.js");
|
|
99
99
|
const user = await cleanupUser(userId);
|
|
100
100
|
const job = getAiCleanupResult(user);
|
|
101
101
|
if (!job)
|
|
@@ -104,21 +104,21 @@ export async function handleAiCleanupApply(userId, body, deps = {}) {
|
|
|
104
104
|
merged: Array.isArray(body?.acceptedMerged) ? body.acceptedMerged : [],
|
|
105
105
|
removed: Array.isArray(body?.acceptedRemoved) ? body.acceptedRemoved : [],
|
|
106
106
|
};
|
|
107
|
-
const
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
}
|
|
119
|
-
log("AI cleanup apply: save conflict", { attempt, of: MAX_PROFILE_SAVE_ATTEMPTS });
|
|
107
|
+
const outcome = await applyToNewestProfile(user, (data) => {
|
|
108
|
+
const applied = applyCleanupEdits(data, job, accepted);
|
|
109
|
+
return {
|
|
110
|
+
profile: applied.profile,
|
|
111
|
+
changed: applied.applied > 0,
|
|
112
|
+
result: { applied: applied.applied, skipped: applied.skipped },
|
|
113
|
+
};
|
|
114
|
+
}, { summary: "AI cleanup applied", store: await managerOf(deps) });
|
|
115
|
+
if (outcome.status === "no-profile")
|
|
116
|
+
return { success: false, error: "Profile not found" };
|
|
117
|
+
if (outcome.status === "conflict") {
|
|
118
|
+
return { success: false, error: "Profile saved at the same moment. Try Apply again." };
|
|
120
119
|
}
|
|
121
|
-
|
|
120
|
+
stopAiCleanupJob(user);
|
|
121
|
+
return { success: true, data: outcome.result };
|
|
122
122
|
}
|
|
123
123
|
catch (error) {
|
|
124
124
|
return handlerError("apply", error);
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { UserProfileManager } from "./user-profile/user-profile-manager.js";
|
|
2
|
+
import type { AutoCleanupSummary } from "./user-profile/auto-cleanup-store.js";
|
|
3
|
+
interface ApiResponse<T = any> {
|
|
4
|
+
success: boolean;
|
|
5
|
+
data?: T;
|
|
6
|
+
error?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface AutoCleanupHandlerDeps {
|
|
9
|
+
manager?: Pick<UserProfileManager, "getActiveProfile" | "updateProfile">;
|
|
10
|
+
}
|
|
11
|
+
/** The newest automatic cleanup that can still be undone, without its items. */
|
|
12
|
+
export declare function handleAutoCleanupStatus(userId?: string, deps?: AutoCleanupHandlerDeps): Promise<ApiResponse<AutoCleanupSummary | null>>;
|
|
13
|
+
/**
|
|
14
|
+
* Undoes the newest automatic cleanup that is not undone, in the newest profile, with the
|
|
15
|
+
* version check. A save conflict is tried again at most three times; after that the profile
|
|
16
|
+
* and the record stay as they were.
|
|
17
|
+
*/
|
|
18
|
+
export declare function handleAutoCleanupUndo(userId?: string, deps?: AutoCleanupHandlerDeps): Promise<ApiResponse<{
|
|
19
|
+
restored: number;
|
|
20
|
+
notFound: number;
|
|
21
|
+
}>>;
|
|
22
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { log } from "./logger.js";
|
|
2
|
+
async function cleanupUser(userId) {
|
|
3
|
+
if (userId)
|
|
4
|
+
return userId;
|
|
5
|
+
const { CONFIG } = await import("../config.js");
|
|
6
|
+
const { resolveWebProfileUserId } = await import("./profile-identity.js");
|
|
7
|
+
return (await resolveWebProfileUserId(CONFIG)) || "unknown";
|
|
8
|
+
}
|
|
9
|
+
async function managerOf(deps) {
|
|
10
|
+
return (deps.manager ?? (await import("./user-profile/user-profile-manager.js")).userProfileManager);
|
|
11
|
+
}
|
|
12
|
+
function handlerError(step, error) {
|
|
13
|
+
// The log never holds profile text, so only the error's kind is kept.
|
|
14
|
+
log(`Automatic cleanup ${step}: error`, {
|
|
15
|
+
code: error instanceof Error ? error.name : "unknown",
|
|
16
|
+
});
|
|
17
|
+
return { success: false, error: String(error) };
|
|
18
|
+
}
|
|
19
|
+
/** The newest automatic cleanup that can still be undone, without its items. */
|
|
20
|
+
export async function handleAutoCleanupStatus(userId, deps = {}) {
|
|
21
|
+
try {
|
|
22
|
+
const { latestOpenAutoCleanupSummary } = await import("./user-profile/auto-cleanup-store.js");
|
|
23
|
+
const profile = await (await managerOf(deps)).getActiveProfile(await cleanupUser(userId));
|
|
24
|
+
if (!profile)
|
|
25
|
+
return { success: true, data: null };
|
|
26
|
+
return { success: true, data: await latestOpenAutoCleanupSummary(profile.id) };
|
|
27
|
+
}
|
|
28
|
+
catch (error) {
|
|
29
|
+
return handlerError("status", error);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Undoes the newest automatic cleanup that is not undone, in the newest profile, with the
|
|
34
|
+
* version check. A save conflict is tried again at most three times; after that the profile
|
|
35
|
+
* and the record stay as they were.
|
|
36
|
+
*/
|
|
37
|
+
export async function handleAutoCleanupUndo(userId, deps = {}) {
|
|
38
|
+
try {
|
|
39
|
+
const { latestOpenAutoCleanup, markAutoCleanupUndone } = await import("./user-profile/auto-cleanup-store.js");
|
|
40
|
+
const { undoCleanupEdits } = await import("./user-profile/ai-cleanup-undo.js");
|
|
41
|
+
const { applyToNewestProfile } = await import("./user-profile/profile-edit-save.js");
|
|
42
|
+
const user = await cleanupUser(userId);
|
|
43
|
+
const manager = await managerOf(deps);
|
|
44
|
+
const profile = await manager.getActiveProfile(user);
|
|
45
|
+
const record = profile ? await latestOpenAutoCleanup(profile.id) : null;
|
|
46
|
+
if (!record)
|
|
47
|
+
return { success: false, error: "Nothing to undo." };
|
|
48
|
+
const outcome = await applyToNewestProfile(user, (data) => {
|
|
49
|
+
const undone = undoCleanupEdits(data, record.entries);
|
|
50
|
+
return {
|
|
51
|
+
profile: undone.profile,
|
|
52
|
+
changed: undone.restored > 0,
|
|
53
|
+
result: { restored: undone.restored, notFound: undone.notFound },
|
|
54
|
+
};
|
|
55
|
+
}, { summary: "Automatic cleanup undone", store: manager });
|
|
56
|
+
if (outcome.status === "no-profile")
|
|
57
|
+
return { success: false, error: "Nothing to undo." };
|
|
58
|
+
if (outcome.status === "conflict") {
|
|
59
|
+
return { success: false, error: "Profile saved at the same moment. Try Undo again." };
|
|
60
|
+
}
|
|
61
|
+
await markAutoCleanupUndone(record.id);
|
|
62
|
+
log("Automatic cleanup undone", {
|
|
63
|
+
restored: outcome.result.restored,
|
|
64
|
+
notFound: outcome.result.notFound,
|
|
65
|
+
});
|
|
66
|
+
return { success: true, data: outcome.result };
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
return handlerError("undo", error);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
@@ -95,6 +95,15 @@ export class CleanupService {
|
|
|
95
95
|
code: error instanceof Error ? error.name : "unknown",
|
|
96
96
|
});
|
|
97
97
|
}
|
|
98
|
+
try {
|
|
99
|
+
const { pruneAutoCleanupRecords } = await import("./user-profile/auto-cleanup-store.js");
|
|
100
|
+
await pruneAutoCleanupRecords();
|
|
101
|
+
}
|
|
102
|
+
catch (error) {
|
|
103
|
+
log("Automatic cleanup record cleanup failed", {
|
|
104
|
+
code: error instanceof Error ? error.name : "unknown",
|
|
105
|
+
});
|
|
106
|
+
}
|
|
98
107
|
try {
|
|
99
108
|
await userPromptManager.vacuum();
|
|
100
109
|
log("Cleanup: VACUUM done", { db: "user-prompts.db" });
|
|
@@ -18,6 +18,8 @@ const keys = new Set([
|
|
|
18
18
|
"opencodeBackfillModel",
|
|
19
19
|
"piBackfillModel",
|
|
20
20
|
"webServerAutoStart",
|
|
21
|
+
"userProfileAutoCleanupEnabled",
|
|
22
|
+
"userProfileAutoCleanupInterval",
|
|
21
23
|
"memoryProvider",
|
|
22
24
|
"memoryApiUrl",
|
|
23
25
|
"memoryModel",
|
|
@@ -25,6 +27,7 @@ const keys = new Set([
|
|
|
25
27
|
"importPathMaps",
|
|
26
28
|
"importIgnoredDirectories",
|
|
27
29
|
"claudeConfigDir",
|
|
30
|
+
"autoCaptureLanguage",
|
|
28
31
|
"maxMemories",
|
|
29
32
|
"chatMessage.maxMemories",
|
|
30
33
|
"autoCaptureMaxContextBytes",
|
|
@@ -69,9 +72,17 @@ function isValidEdit(key, value) {
|
|
|
69
72
|
if (key === "embeddingDimensions") {
|
|
70
73
|
return Number.isInteger(value) && value > 0 && value <= 65536;
|
|
71
74
|
}
|
|
72
|
-
if (key === "captureTrace" ||
|
|
75
|
+
if (key === "captureTrace" ||
|
|
76
|
+
key === "autoBackfill" ||
|
|
77
|
+
key === "webServerAutoStart" ||
|
|
78
|
+
key === "userProfileAutoCleanupEnabled") {
|
|
73
79
|
return typeof value === "boolean";
|
|
74
80
|
}
|
|
81
|
+
// Each automatic cleanup is paid for, so the page refuses an interval that would run one
|
|
82
|
+
// after almost every learning pass. A hand edit of the file has no floor.
|
|
83
|
+
if (key === "userProfileAutoCleanupInterval") {
|
|
84
|
+
return Number.isSafeInteger(value) && value >= 50 && value <= 100000;
|
|
85
|
+
}
|
|
75
86
|
if (key.endsWith("RetentionDays"))
|
|
76
87
|
return Number.isSafeInteger(value) && value >= 1;
|
|
77
88
|
if (key === "captureRetryRetentionHours") {
|
|
@@ -81,6 +92,9 @@ function isValidEdit(key, value) {
|
|
|
81
92
|
return isSecretReference(value);
|
|
82
93
|
if (key === "memoryProvider")
|
|
83
94
|
return MEMORY_PROVIDERS.includes(value);
|
|
95
|
+
// "auto", or an ISO 639 code (en, id, zh, fil).
|
|
96
|
+
if (key === "autoCaptureLanguage")
|
|
97
|
+
return typeof value === "string" && /^(auto|[a-z]{2,3})$/.test(value);
|
|
84
98
|
// Entries are checked by the startup validation below.
|
|
85
99
|
if (key === "importPathMaps" || key === "importIgnoredDirectories")
|
|
86
100
|
return Array.isArray(value);
|