om-memory-system 4.6.0-next.83 → 4.7.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/dist/adapters/opencode/profile-learning.js +11 -6
- package/dist/adapters/pi/extension.js +11 -2
- package/dist/config.d.ts +7 -3
- package/dist/config.js +52 -9
- package/dist/core/capture.js +7 -3
- package/dist/core/context-budget.d.ts +74 -0
- package/dist/core/context-budget.js +98 -0
- package/dist/core/memory-operations.js +4 -1
- package/dist/core/retrieval.d.ts +8 -3
- package/dist/core/retrieval.js +27 -16
- package/dist/importer/claude-hook-api.js +14 -5
- package/dist/index.js +27 -5
- package/dist/services/client.js +5 -1
- package/dist/services/context.d.ts +9 -1
- package/dist/services/context.js +44 -16
- package/dist/services/global-config-writer.js +18 -2
- package/dist/services/settings-snapshot.d.ts +1 -7
- package/dist/services/settings-snapshot.js +49 -2
- package/dist/services/web-ensure.js +27 -15
- package/dist/utils/memory-limits.d.ts +34 -0
- package/dist/utils/memory-limits.js +40 -0
- package/dist/v2/adapter.js +28 -10
- package/dist/v2/memory-bridge.d.ts +12 -4
- package/dist/v2/memory-bridge.js +22 -5
- package/dist/web/assets/index-Biht9O0s.css +2 -0
- package/dist/web/assets/{index-BEwbs3t2.js → index-BpmYx_6Z.js} +6 -6
- package/dist/web/index.html +2 -2
- package/package.json +1 -1
- package/dist/web/assets/index-BYLF18Gt.css +0 -2
|
@@ -6,6 +6,7 @@ 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 { sortProfileItems } from "../../utils/profile.js";
|
|
9
|
+
import { truncateToMaxBytes } from "../../utils/context-limit.js";
|
|
9
10
|
import { loadOpencodeProvider } from "./opencode-provider-loader.js";
|
|
10
11
|
let isLearningRunning = false;
|
|
11
12
|
export function shouldRunAutomaticProfileCleanup(previousPromptCount, addedPromptCount, interval = CONFIG.userProfileAutoCleanupInterval) {
|
|
@@ -46,6 +47,10 @@ export async function performUserProfileLearning(ctx, directory) {
|
|
|
46
47
|
if (!profileBackoff.canRun())
|
|
47
48
|
return;
|
|
48
49
|
refreshConfigIfChanged(directory);
|
|
50
|
+
// Snapshot the byte limit right after the refresh: the prompt and profile
|
|
51
|
+
// reads below all await, and a config edit during them must not change
|
|
52
|
+
// this pass's input limit.
|
|
53
|
+
const profileContextMaxBytes = CONFIG.userProfileMaxContextBytes ?? 32768;
|
|
49
54
|
if (!CONFIG.autoCaptureProviderStatus || !CONFIG.autoCaptureProviderStatus.ready) {
|
|
50
55
|
log("user-profile-learning: skipped (provider not ready)", {
|
|
51
56
|
issues: CONFIG.autoCaptureProviderStatus?.issues ?? ["status undefined"],
|
|
@@ -134,7 +139,7 @@ Rules:
|
|
|
134
139
|
}
|
|
135
140
|
existingProfile = { ...existingProfile, profileData: JSON.stringify(decayed) };
|
|
136
141
|
}
|
|
137
|
-
const context = buildUserAnalysisContext(prompts, existingProfile, validationPrompt);
|
|
142
|
+
const context = buildUserAnalysisContext(prompts, existingProfile, validationPrompt, profileContextMaxBytes);
|
|
138
143
|
const analysisResult = await analyzeUserProfile(context, existingProfile);
|
|
139
144
|
profileBackoff.recordSuccess();
|
|
140
145
|
log("user-profile-learning: analyze done", { hasResult: !!analysisResult });
|
|
@@ -282,7 +287,7 @@ function buildCategorySummary(profileData) {
|
|
|
282
287
|
New observations are matched via embedding cosine similarity — write descriptions in your own words; do not reuse existing wording.`;
|
|
283
288
|
return [catSection, ...wfParts, countSection].filter(Boolean).join("\n");
|
|
284
289
|
}
|
|
285
|
-
function buildUserAnalysisContext(prompts, existingProfile, validationPrompt) {
|
|
290
|
+
function buildUserAnalysisContext(prompts, existingProfile, validationPrompt, maxBytes = CONFIG.userProfileMaxContextBytes ?? 32768) {
|
|
286
291
|
const base = `# User Profile Analysis
|
|
287
292
|
|
|
288
293
|
Analyze ${prompts.length} user prompts to ${existingProfile ? "update" : "create"} the user profile.
|
|
@@ -336,10 +341,10 @@ CRITICAL: Only output observations grounded in the RECENT PROMPTS above. Write d
|
|
|
336
341
|
❌ Do NOT extract as workflow:
|
|
337
342
|
- "User analyzes problems and verifies solutions" (too abstract — not a concrete step sequence)
|
|
338
343
|
- "User writes code and tests it" (too generic — covers everything)`;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
344
|
+
// UTF-8 byte limit: the marker counts inside the ceiling and a cut never
|
|
345
|
+
// splits a multi-byte character (a character count let multibyte input
|
|
346
|
+
// through oversized and could split surrogate pairs at the cut).
|
|
347
|
+
const truncate = (s) => truncateToMaxBytes(s, maxBytes, "\n[... context truncated to userProfileMaxContextBytes ...]");
|
|
343
348
|
if (validationPrompt) {
|
|
344
349
|
return truncate(base + "\n\n" + validationPrompt);
|
|
345
350
|
}
|
|
@@ -11,7 +11,8 @@ import { capturePiSettledWorkUnit, createPiCaptureState } from "./capture.js";
|
|
|
11
11
|
import { createPiLiveModels } from "./live-model.js";
|
|
12
12
|
import { registerPiHistoryImportCommand } from "./import-command.js";
|
|
13
13
|
import { performPiProfileLearning } from "./profile.js";
|
|
14
|
-
import { buildRetrievalSection, wrapRetrievalSection } from "../../core/retrieval.js";
|
|
14
|
+
import { buildRetrievalSection, retrievalWrapperBytes, wrapRetrievalSection, } from "../../core/retrieval.js";
|
|
15
|
+
import { DEFAULT_RETRIEVAL_MAX_TOKENS, tokensToByteCeiling } from "../../core/context-budget.js";
|
|
15
16
|
const GLOBAL_PLUGIN_WARMUP_KEY = Symbol.for("omms.plugin.warmedup");
|
|
16
17
|
const GLOBAL_PI_BACKFILL_KEY = Symbol.for("omms.pi.backfill.scheduled");
|
|
17
18
|
const OMMS_STATUS_KEY = "omms";
|
|
@@ -224,11 +225,19 @@ export default function ommsPiExtension(pi) {
|
|
|
224
225
|
}
|
|
225
226
|
});
|
|
226
227
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
228
|
+
// Refresh first so the enabled check reads the current file, then capture
|
|
229
|
+
// this retrieval's allowance before the async search starts: a later
|
|
230
|
+
// config edit must not change an operation already in progress.
|
|
231
|
+
refreshConfigIfChanged(ctx.cwd);
|
|
232
|
+
// The default guards partial CONFIG stubs in tests.
|
|
233
|
+
const budgetBytes = tokensToByteCeiling(CONFIG.retrievalMaxTokens ?? DEFAULT_RETRIEVAL_MAX_TOKENS);
|
|
227
234
|
if (!isConfigured() || !CONFIG.chatMessage.enabled)
|
|
228
235
|
return;
|
|
229
236
|
const status = startStatus(ctx, "recalling");
|
|
230
237
|
try {
|
|
231
|
-
|
|
238
|
+
// The wrapper counts against the captured allowance, so the packed
|
|
239
|
+
// section reserves its bytes.
|
|
240
|
+
const section = await buildRetrievalSection(event.prompt, ctx.cwd, ctx.sessionManager.getSessionId(), { maxBytes: budgetBytes, wrapperBytes: retrievalWrapperBytes() });
|
|
232
241
|
finishStatus(ctx, status, "connected");
|
|
233
242
|
if (!section)
|
|
234
243
|
return;
|
package/dist/config.d.ts
CHANGED
|
@@ -14,6 +14,8 @@ interface OmmsConfig {
|
|
|
14
14
|
embeddingUseTaskPrefixes?: boolean;
|
|
15
15
|
similarityThreshold?: number;
|
|
16
16
|
maxMemories?: number;
|
|
17
|
+
/** Shared budget for automatic memory context in approximate tokens (ceil(UTF-8 bytes / 4)). */
|
|
18
|
+
retrievalMaxTokens?: number;
|
|
17
19
|
maxProfileItems?: number;
|
|
18
20
|
injectProfile?: boolean;
|
|
19
21
|
containerTagPrefix?: string;
|
|
@@ -102,7 +104,7 @@ interface OmmsConfig {
|
|
|
102
104
|
injectOn?: "first" | "always";
|
|
103
105
|
};
|
|
104
106
|
}
|
|
105
|
-
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 // 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 // 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 \"maxMemories\": 10,\n\n // ============================================\n // Advanced Settings\n // ============================================\n \n // Inject user profile into AI context (preferences, patterns, workflows)\n \"injectProfile\": true\n}\n";
|
|
107
|
+
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 // 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 // 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";
|
|
106
108
|
export declare function normalizeAutoCaptureMaxContextBytes(value: number): number;
|
|
107
109
|
/** Whole hours from 0 to 720. A value that is not a number gives the default. */
|
|
108
110
|
export declare function normalizeCaptureRetryRetentionHours(value: unknown): number;
|
|
@@ -118,6 +120,7 @@ declare function buildConfig(fileConfig: OmmsConfig): {
|
|
|
118
120
|
embeddingApiKey: string | undefined;
|
|
119
121
|
similarityThreshold: number;
|
|
120
122
|
maxMemories: number;
|
|
123
|
+
retrievalMaxTokens: number;
|
|
121
124
|
maxProfileItems: number;
|
|
122
125
|
injectProfile: boolean;
|
|
123
126
|
containerTagPrefix: string;
|
|
@@ -193,7 +196,7 @@ declare function buildConfig(fileConfig: OmmsConfig): {
|
|
|
193
196
|
};
|
|
194
197
|
chatMessage: {
|
|
195
198
|
enabled: boolean | undefined;
|
|
196
|
-
maxMemories: number
|
|
199
|
+
maxMemories: number;
|
|
197
200
|
excludeCurrentSession: boolean | undefined;
|
|
198
201
|
maxAgeDays: number | undefined;
|
|
199
202
|
injectOn: "first" | "always";
|
|
@@ -213,6 +216,7 @@ export declare let CONFIG: {
|
|
|
213
216
|
embeddingApiKey: string | undefined;
|
|
214
217
|
similarityThreshold: number;
|
|
215
218
|
maxMemories: number;
|
|
219
|
+
retrievalMaxTokens: number;
|
|
216
220
|
maxProfileItems: number;
|
|
217
221
|
injectProfile: boolean;
|
|
218
222
|
containerTagPrefix: string;
|
|
@@ -288,7 +292,7 @@ export declare let CONFIG: {
|
|
|
288
292
|
};
|
|
289
293
|
chatMessage: {
|
|
290
294
|
enabled: boolean | undefined;
|
|
291
|
-
maxMemories: number
|
|
295
|
+
maxMemories: number;
|
|
292
296
|
excludeCurrentSession: boolean | undefined;
|
|
293
297
|
maxAgeDays: number | undefined;
|
|
294
298
|
injectOn: "first" | "always";
|
package/dist/config.js
CHANGED
|
@@ -9,6 +9,7 @@ import { isPlaceholderApiKey } from "./services/ai/api-key-placeholder.js";
|
|
|
9
9
|
import { getAutoCaptureProviderStatus } from "./services/ai/live-model-choice.js";
|
|
10
10
|
import { parseBackfillModel } from "./importer/backfill-model.js";
|
|
11
11
|
import { parseImportPathMaps } from "./importer/import-path-maps.js";
|
|
12
|
+
import { checkedMemoryLimit, MEMORY_LIMITS } from "./utils/memory-limits.js";
|
|
12
13
|
import { resolveDefaultStoragePath, runLegacyStoreMigration, legacyMigrationPaths, } from "./services/legacy-migration.js";
|
|
13
14
|
const OMMS_CONFIG_DIR = join(homedir(), ".config", "omms");
|
|
14
15
|
const DATA_DIR = join(homedir(), ".omms");
|
|
@@ -39,6 +40,7 @@ const DEFAULTS = {
|
|
|
39
40
|
embeddingUseTaskPrefixes: false,
|
|
40
41
|
similarityThreshold: 0.6,
|
|
41
42
|
maxMemories: 10,
|
|
43
|
+
retrievalMaxTokens: 2000,
|
|
42
44
|
maxProfileItems: 5,
|
|
43
45
|
injectProfile: true,
|
|
44
46
|
// omms since the tag prefix migration: new memories carry the fork's
|
|
@@ -493,9 +495,35 @@ export const CONFIG_TEMPLATE = `{
|
|
|
493
495
|
// Minimum similarity score (0-1) for memory search results
|
|
494
496
|
"similarityThreshold": 0.6,
|
|
495
497
|
|
|
496
|
-
// Maximum number of memories to return in search results
|
|
498
|
+
// Maximum number of memories to return in search results.
|
|
499
|
+
// Manual searches can ask for fewer; prompt retrieval uses this ceiling.
|
|
497
500
|
"maxMemories": 10,
|
|
498
501
|
|
|
502
|
+
// ============================================
|
|
503
|
+
// Memory Context Controls
|
|
504
|
+
// ============================================
|
|
505
|
+
|
|
506
|
+
// Recent memories added at fresh session start (OpenCode V1 and Claude Code).
|
|
507
|
+
// Pi and OpenCode V2 search the store with each prompt instead.
|
|
508
|
+
"chatMessage": {
|
|
509
|
+
"enabled": true,
|
|
510
|
+
// Recent memories injected at session start.
|
|
511
|
+
"maxMemories": 3,
|
|
512
|
+
"excludeCurrentSession": true,
|
|
513
|
+
// When to inject: "first" message of a session or "always".
|
|
514
|
+
// "injectOn": "first",
|
|
515
|
+
},
|
|
516
|
+
|
|
517
|
+
// OpenCode profile-learning input limit, in UTF-8 bytes. Smaller values can
|
|
518
|
+
// omit prompts from that input. Does not limit Pi or Claude Code profile input.
|
|
519
|
+
"userProfileMaxContextBytes": 32768,
|
|
520
|
+
|
|
521
|
+
// Shared budget for automatic memory context (profile text, memories, and
|
|
522
|
+
// formatting) across all hosts, in approximate tokens: ceil(UTF-8 bytes / 4).
|
|
523
|
+
// Covers prompt retrieval, recent-memory injection, and session-memory
|
|
524
|
+
// restoration. Not an exact provider token limit or a spending limit.
|
|
525
|
+
"retrievalMaxTokens": 2000,
|
|
526
|
+
|
|
499
527
|
// ============================================
|
|
500
528
|
// Advanced Settings
|
|
501
529
|
// ============================================
|
|
@@ -563,10 +591,18 @@ function getEmbeddingDimensions(model) {
|
|
|
563
591
|
return dimensionMap[model] || 768;
|
|
564
592
|
}
|
|
565
593
|
export function normalizeAutoCaptureMaxContextBytes(value) {
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
594
|
+
return checkedMemoryLimit(MEMORY_LIMITS.autoCaptureMaxContextBytes, value);
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Validate a top-level memory limit that is present in the file. A missing
|
|
598
|
+
* key uses the default, so only own properties reach the check and a null
|
|
599
|
+
* value is rejected instead of silently becoming the default.
|
|
600
|
+
*/
|
|
601
|
+
function checkedOwnMemoryLimit(rule, container, key, fallback) {
|
|
602
|
+
const value = Object.hasOwn(container, key)
|
|
603
|
+
? container[key]
|
|
604
|
+
: fallback;
|
|
605
|
+
return checkedMemoryLimit(rule, value);
|
|
570
606
|
}
|
|
571
607
|
/** Whole hours from 0 to 720. A value that is not a number gives the default. */
|
|
572
608
|
export function normalizeCaptureRetryRetentionHours(value) {
|
|
@@ -599,7 +635,13 @@ function buildConfig(fileConfig) {
|
|
|
599
635
|
const memoryApiKey = resolveMemoryApiKey(fileConfig.memoryApiKey);
|
|
600
636
|
const embeddingDimensions = fileConfig.embeddingDimensions ??
|
|
601
637
|
getEmbeddingDimensions(fileConfig.embeddingModel ?? DEFAULTS.embeddingModel);
|
|
602
|
-
const autoCaptureMaxContextBytes =
|
|
638
|
+
const autoCaptureMaxContextBytes = checkedOwnMemoryLimit(MEMORY_LIMITS.autoCaptureMaxContextBytes, fileConfig, "autoCaptureMaxContextBytes", DEFAULTS.autoCaptureMaxContextBytes);
|
|
639
|
+
const maxMemories = checkedOwnMemoryLimit(MEMORY_LIMITS.maxMemories, fileConfig, "maxMemories", DEFAULTS.maxMemories);
|
|
640
|
+
const retrievalMaxTokens = checkedOwnMemoryLimit(MEMORY_LIMITS.retrievalMaxTokens, fileConfig, "retrievalMaxTokens", DEFAULTS.retrievalMaxTokens);
|
|
641
|
+
const userProfileMaxContextBytes = checkedOwnMemoryLimit(MEMORY_LIMITS.userProfileMaxContextBytes, fileConfig, "userProfileMaxContextBytes", DEFAULTS.userProfileMaxContextBytes);
|
|
642
|
+
const chatMessageMaxMemories = checkedMemoryLimit(MEMORY_LIMITS["chatMessage.maxMemories"], fileConfig.chatMessage && Object.hasOwn(fileConfig.chatMessage, "maxMemories")
|
|
643
|
+
? fileConfig.chatMessage.maxMemories
|
|
644
|
+
: DEFAULTS.chatMessage.maxMemories);
|
|
603
645
|
const userProfileAutoCleanupInterval = fileConfig.userProfileAutoCleanupInterval ?? DEFAULTS.userProfileAutoCleanupInterval;
|
|
604
646
|
parseBackfillModel(fileConfig, "pi");
|
|
605
647
|
parseBackfillModel(fileConfig, "opencode");
|
|
@@ -636,7 +678,8 @@ function buildConfig(fileConfig) {
|
|
|
636
678
|
resolveSecretValue(fileConfig.embeddingApiKey)
|
|
637
679
|
: undefined,
|
|
638
680
|
similarityThreshold: fileConfig.similarityThreshold ?? DEFAULTS.similarityThreshold,
|
|
639
|
-
maxMemories
|
|
681
|
+
maxMemories,
|
|
682
|
+
retrievalMaxTokens,
|
|
640
683
|
maxProfileItems: fileConfig.maxProfileItems ?? DEFAULTS.maxProfileItems,
|
|
641
684
|
injectProfile: fileConfig.injectProfile ?? DEFAULTS.injectProfile,
|
|
642
685
|
containerTagPrefix: fileConfig.containerTagPrefix ?? DEFAULTS.containerTagPrefix,
|
|
@@ -689,7 +732,7 @@ function buildConfig(fileConfig) {
|
|
|
689
732
|
deduplicationEnabled: fileConfig.deduplicationEnabled ?? DEFAULTS.deduplicationEnabled,
|
|
690
733
|
deduplicationSimilarityThreshold: fileConfig.deduplicationSimilarityThreshold ?? DEFAULTS.deduplicationSimilarityThreshold,
|
|
691
734
|
userProfileAnalysisInterval: fileConfig.userProfileAnalysisInterval ?? DEFAULTS.userProfileAnalysisInterval,
|
|
692
|
-
userProfileMaxContextBytes
|
|
735
|
+
userProfileMaxContextBytes,
|
|
693
736
|
userProfileDisplayPreferences: fileConfig.userProfileDisplayPreferences ?? DEFAULTS.userProfileDisplayPreferences,
|
|
694
737
|
userProfileDisplayPatterns: fileConfig.userProfileDisplayPatterns ?? DEFAULTS.userProfileDisplayPatterns,
|
|
695
738
|
userProfileDisplayWorkflows: fileConfig.userProfileDisplayWorkflows ?? DEFAULTS.userProfileDisplayWorkflows,
|
|
@@ -726,7 +769,7 @@ function buildConfig(fileConfig) {
|
|
|
726
769
|
},
|
|
727
770
|
chatMessage: {
|
|
728
771
|
enabled: fileConfig.chatMessage?.enabled ?? DEFAULTS.chatMessage.enabled,
|
|
729
|
-
maxMemories:
|
|
772
|
+
maxMemories: chatMessageMaxMemories,
|
|
730
773
|
excludeCurrentSession: fileConfig.chatMessage?.excludeCurrentSession ?? DEFAULTS.chatMessage.excludeCurrentSession,
|
|
731
774
|
maxAgeDays: fileConfig.chatMessage?.maxAgeDays,
|
|
732
775
|
injectOn: (fileConfig.chatMessage?.injectOn ?? DEFAULTS.chatMessage.injectOn),
|
package/dist/core/capture.js
CHANGED
|
@@ -36,11 +36,15 @@ async function getLatestProjectMemory(containerTag) {
|
|
|
36
36
|
*/
|
|
37
37
|
export async function captureConversation(workUnit, provider) {
|
|
38
38
|
refreshConfigIfChanged(workUnit.projectDirectory);
|
|
39
|
+
// Snapshot the markdown budget after the refresh and before the first
|
|
40
|
+
// await: a config edit during the memory read must not change this
|
|
41
|
+
// capture's input limit. The budget keeps its built-in request reserve.
|
|
42
|
+
const markdownBudget = getAutoCaptureMarkdownBudget();
|
|
39
43
|
const diagnostics = {};
|
|
40
44
|
const startedAt = Date.now();
|
|
41
45
|
let outcome = "failed";
|
|
42
46
|
try {
|
|
43
|
-
const result = await runCapture(workUnit, provider, diagnostics);
|
|
47
|
+
const result = await runCapture(workUnit, provider, diagnostics, markdownBudget);
|
|
44
48
|
outcome = result.status === "captured" ? "saved" : "skipped";
|
|
45
49
|
return result;
|
|
46
50
|
}
|
|
@@ -60,10 +64,10 @@ export async function captureConversation(workUnit, provider) {
|
|
|
60
64
|
emitCaptureAttempt(record, diagnostics, CONFIG);
|
|
61
65
|
}
|
|
62
66
|
}
|
|
63
|
-
async function runCapture(workUnit, provider, diagnostics) {
|
|
67
|
+
async function runCapture(workUnit, provider, diagnostics, markdownBudget) {
|
|
64
68
|
const tags = getTags(workUnit.projectDirectory);
|
|
65
69
|
const latestMemory = await getLatestProjectMemory(tags.project.tag);
|
|
66
|
-
const context = buildMarkdownContext(workUnit.userPrompt, workUnit.textResponses, workUnit.toolCalls, latestMemory,
|
|
70
|
+
const context = buildMarkdownContext(workUnit.userPrompt, workUnit.textResponses, workUnit.toolCalls, latestMemory, markdownBudget);
|
|
67
71
|
let summaryResult;
|
|
68
72
|
try {
|
|
69
73
|
summaryResult = await provider.summarize({
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Default approximate-token budget for automatically injected memory context.
|
|
3
|
+
* Hosts pass their snapshotted `CONFIG.retrievalMaxTokens` explicitly; this
|
|
4
|
+
* numeric default keeps the shared formatters host-neutral.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_RETRIEVAL_MAX_TOKENS = 2000;
|
|
7
|
+
/** Approximate token estimate: ceil(UTF-8 byte length / 4). Never provider-exact. */
|
|
8
|
+
export declare function estimateTokens(text: string): number;
|
|
9
|
+
/** Internal UTF-8 byte ceiling for an approximate-token budget. */
|
|
10
|
+
export declare function tokensToByteCeiling(maxTokens: number): number;
|
|
11
|
+
/**
|
|
12
|
+
* Budget options every automatic-memory formatter accepts.
|
|
13
|
+
* `maxBytes` is an explicit byte ceiling (a residual allowance shared with
|
|
14
|
+
* other sections in the same host request) and wins over `maxTokens`.
|
|
15
|
+
* `wrapperBytes` reserves the bytes of a host wrapper, such as the
|
|
16
|
+
* `omms-retrieval` tag, added around the emitted section.
|
|
17
|
+
*/
|
|
18
|
+
export interface ContextBudgetOptions {
|
|
19
|
+
maxTokens?: number;
|
|
20
|
+
maxBytes?: number;
|
|
21
|
+
wrapperBytes?: number;
|
|
22
|
+
}
|
|
23
|
+
/** One variable-content entry: fixed delimiters around a body that may be shortened. */
|
|
24
|
+
export interface BudgetedEntry {
|
|
25
|
+
prefix: string;
|
|
26
|
+
body: string;
|
|
27
|
+
suffix: string;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* A group of entries wrapped in fixed delimiters, such as the profile block or
|
|
31
|
+
* the `<project_knowledge>` block. `maxShare` caps the block at a fraction of
|
|
32
|
+
* the payload space left after fixed formatting; the profile uses 0.25.
|
|
33
|
+
*/
|
|
34
|
+
export interface BudgetedBlock {
|
|
35
|
+
prefix: string;
|
|
36
|
+
suffix: string;
|
|
37
|
+
/** Separator emitted before every entry, including the first. */
|
|
38
|
+
entrySeparator?: string;
|
|
39
|
+
/** Separator emitted between the last entry and the block suffix. */
|
|
40
|
+
suffixSeparator?: string;
|
|
41
|
+
entries: BudgetedEntry[];
|
|
42
|
+
maxShare?: number;
|
|
43
|
+
}
|
|
44
|
+
export interface PackContextSectionOptions extends ContextBudgetOptions {
|
|
45
|
+
/** Fixed section header emitted before all payload blocks. */
|
|
46
|
+
header: string;
|
|
47
|
+
/** Fixed section footer emitted after all payload blocks. */
|
|
48
|
+
footer?: string;
|
|
49
|
+
/** Separator between payload blocks. */
|
|
50
|
+
blockSeparator?: string;
|
|
51
|
+
/** Payload blocks in emission order: profile first, then memories. */
|
|
52
|
+
blocks: BudgetedBlock[];
|
|
53
|
+
/** Visible omission marker used when shortening a body. */
|
|
54
|
+
marker?: string;
|
|
55
|
+
}
|
|
56
|
+
export interface PackedSection {
|
|
57
|
+
/** Assembled section text; empty when no useful content fits the budget. */
|
|
58
|
+
text: string;
|
|
59
|
+
/** UTF-8 bytes of `text` (the host wrapper is not part of `text`). */
|
|
60
|
+
bytesUsed: number;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Pack payload blocks into a section that fits one byte ceiling.
|
|
64
|
+
*
|
|
65
|
+
* Fixed headers, footers, delimiters, and the host wrapper are reserved before
|
|
66
|
+
* any payload space is assigned. Blocks with `maxShare` (the profile) are
|
|
67
|
+
* shortened inside their share and never take more; space they leave unused
|
|
68
|
+
* stays available to later blocks. Entries keep their incoming order: each is
|
|
69
|
+
* kept whole while it fits, the first oversized entry is shortened with a
|
|
70
|
+
* visible marker and packing stops there, and later entries never displace it.
|
|
71
|
+
* The assembled text contains only complete delimiters and valid Unicode, and
|
|
72
|
+
* `bytesUsed + wrapperBytes` never exceeds the ceiling.
|
|
73
|
+
*/
|
|
74
|
+
export declare function packContextSection(options: PackContextSectionOptions): PackedSection;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { truncateToMaxBytes, utf8ByteLength } from "../utils/context-limit.js";
|
|
2
|
+
/**
|
|
3
|
+
* Default approximate-token budget for automatically injected memory context.
|
|
4
|
+
* Hosts pass their snapshotted `CONFIG.retrievalMaxTokens` explicitly; this
|
|
5
|
+
* numeric default keeps the shared formatters host-neutral.
|
|
6
|
+
*/
|
|
7
|
+
export const DEFAULT_RETRIEVAL_MAX_TOKENS = 2000;
|
|
8
|
+
// Must match the default marker in src/utils/context-limit.ts: the packer needs
|
|
9
|
+
// its byte size to reserve room for a visible omission marker.
|
|
10
|
+
const DEFAULT_TRUNCATION_MARKER = "\n[... truncated ...]\n";
|
|
11
|
+
/** Approximate token estimate: ceil(UTF-8 byte length / 4). Never provider-exact. */
|
|
12
|
+
export function estimateTokens(text) {
|
|
13
|
+
return Math.ceil(utf8ByteLength(text) / 4);
|
|
14
|
+
}
|
|
15
|
+
/** Internal UTF-8 byte ceiling for an approximate-token budget. */
|
|
16
|
+
export function tokensToByteCeiling(maxTokens) {
|
|
17
|
+
return maxTokens * 4;
|
|
18
|
+
}
|
|
19
|
+
function resolveCeiling(options) {
|
|
20
|
+
if (options.maxBytes !== undefined)
|
|
21
|
+
return options.maxBytes;
|
|
22
|
+
return tokensToByteCeiling(options.maxTokens ?? DEFAULT_RETRIEVAL_MAX_TOKENS);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Pack payload blocks into a section that fits one byte ceiling.
|
|
26
|
+
*
|
|
27
|
+
* Fixed headers, footers, delimiters, and the host wrapper are reserved before
|
|
28
|
+
* any payload space is assigned. Blocks with `maxShare` (the profile) are
|
|
29
|
+
* shortened inside their share and never take more; space they leave unused
|
|
30
|
+
* stays available to later blocks. Entries keep their incoming order: each is
|
|
31
|
+
* kept whole while it fits, the first oversized entry is shortened with a
|
|
32
|
+
* visible marker and packing stops there, and later entries never displace it.
|
|
33
|
+
* The assembled text contains only complete delimiters and valid Unicode, and
|
|
34
|
+
* `bytesUsed + wrapperBytes` never exceeds the ceiling.
|
|
35
|
+
*/
|
|
36
|
+
export function packContextSection(options) {
|
|
37
|
+
const marker = options.marker ?? DEFAULT_TRUNCATION_MARKER;
|
|
38
|
+
const markerBytes = utf8ByteLength(marker);
|
|
39
|
+
const wrapperBytes = options.wrapperBytes ?? 0;
|
|
40
|
+
const blockSeparator = options.blockSeparator ?? "";
|
|
41
|
+
const payload = resolveCeiling(options) -
|
|
42
|
+
wrapperBytes -
|
|
43
|
+
utf8ByteLength(options.header) -
|
|
44
|
+
utf8ByteLength(options.footer ?? "");
|
|
45
|
+
if (payload <= 0)
|
|
46
|
+
return { text: "", bytesUsed: 0 };
|
|
47
|
+
const emittedBlocks = [];
|
|
48
|
+
let used = 0;
|
|
49
|
+
for (const block of options.blocks) {
|
|
50
|
+
const separatorBytes = emittedBlocks.length > 0 ? utf8ByteLength(blockSeparator) : 0;
|
|
51
|
+
let blockBudget = payload - used - separatorBytes;
|
|
52
|
+
if (block.maxShare !== undefined) {
|
|
53
|
+
blockBudget = Math.min(blockBudget, Math.floor(payload * block.maxShare));
|
|
54
|
+
}
|
|
55
|
+
if (blockBudget <= 0)
|
|
56
|
+
break;
|
|
57
|
+
const emittedEntries = [];
|
|
58
|
+
const entrySeparator = block.entrySeparator ?? "";
|
|
59
|
+
const entrySeparatorBytes = utf8ByteLength(entrySeparator);
|
|
60
|
+
const suffixSeparator = block.suffixSeparator ?? "";
|
|
61
|
+
let blockUsed = utf8ByteLength(block.prefix) + utf8ByteLength(block.suffix) + utf8ByteLength(suffixSeparator);
|
|
62
|
+
for (const entry of block.entries) {
|
|
63
|
+
const available = blockBudget - blockUsed - entrySeparatorBytes;
|
|
64
|
+
if (available <= 0)
|
|
65
|
+
break;
|
|
66
|
+
const entryFixed = utf8ByteLength(entry.prefix) + utf8ByteLength(entry.suffix);
|
|
67
|
+
const bodyBytes = utf8ByteLength(entry.body);
|
|
68
|
+
if (entryFixed + bodyBytes <= available) {
|
|
69
|
+
emittedEntries.push(entry.prefix + entry.body + entry.suffix);
|
|
70
|
+
blockUsed += entrySeparatorBytes + entryFixed + bodyBytes;
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
// First oversized entry: shorten its body with a visible marker, then stop.
|
|
74
|
+
const bodyAllowance = available - entryFixed;
|
|
75
|
+
if (bodyAllowance <= markerBytes)
|
|
76
|
+
break; // no room for content plus marker
|
|
77
|
+
const shortened = truncateToMaxBytes(entry.body, bodyAllowance, marker);
|
|
78
|
+
if (shortened.length === 0 || shortened === marker)
|
|
79
|
+
break;
|
|
80
|
+
emittedEntries.push(entry.prefix + shortened + entry.suffix);
|
|
81
|
+
blockUsed += entrySeparatorBytes + entryFixed + utf8ByteLength(shortened);
|
|
82
|
+
break;
|
|
83
|
+
}
|
|
84
|
+
if (emittedEntries.length === 0)
|
|
85
|
+
continue; // drop the block; it owes no separator
|
|
86
|
+
const blockText = block.prefix +
|
|
87
|
+
emittedEntries.map((entry) => entrySeparator + entry).join("") +
|
|
88
|
+
suffixSeparator +
|
|
89
|
+
block.suffix;
|
|
90
|
+
emittedBlocks.push(blockText);
|
|
91
|
+
used += separatorBytes + blockUsed;
|
|
92
|
+
}
|
|
93
|
+
if (emittedBlocks.length === 0)
|
|
94
|
+
return { text: "", bytesUsed: 0 };
|
|
95
|
+
const text = options.header + emittedBlocks.join(blockSeparator) + (options.footer ?? "");
|
|
96
|
+
const bytesUsed = utf8ByteLength(text);
|
|
97
|
+
return { text, bytesUsed };
|
|
98
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CONFIG, isConfigured } from "../config.js";
|
|
1
|
+
import { CONFIG, isConfigured, refreshConfigIfChanged } from "../config.js";
|
|
2
2
|
import { memoryClient } from "../services/client.js";
|
|
3
3
|
import { getLanguageName } from "../services/language-detector.js";
|
|
4
4
|
import { stripPrivateContent, isFullyPrivate } from "../services/privacy.js";
|
|
@@ -18,6 +18,9 @@ function formatSearchResults(query, results, limit) {
|
|
|
18
18
|
};
|
|
19
19
|
}
|
|
20
20
|
export async function executeMemoryOperation(args, context) {
|
|
21
|
+
// The next operation sees a changed config file: manual tool and CLI calls
|
|
22
|
+
// happen outside the hosts' injection and capture boundaries.
|
|
23
|
+
refreshConfigIfChanged(context.directory);
|
|
21
24
|
if (!isConfigured()) {
|
|
22
25
|
return {
|
|
23
26
|
success: false,
|
package/dist/core/retrieval.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type ContextBudgetOptions } from "./context-budget.js";
|
|
1
2
|
export declare const RETRIEVAL_SECTION_TAG = "omms-retrieval";
|
|
2
3
|
/**
|
|
3
4
|
* Prompt-aware semantic retrieval shared by every host (Pi `before_agent_start`,
|
|
@@ -5,7 +6,7 @@ export declare const RETRIEVAL_SECTION_TAG = "omms-retrieval";
|
|
|
5
6
|
* the incoming prompt and format a bounded context section (memory + user
|
|
6
7
|
* profile). Returns null when nothing relevant is found.
|
|
7
8
|
*/
|
|
8
|
-
export declare function buildRetrievalSection(prompt: string, directory: string, sessionId: string): Promise<string | null>;
|
|
9
|
+
export declare function buildRetrievalSection(prompt: string, directory: string, sessionId: string, budget?: ContextBudgetOptions): Promise<string | null>;
|
|
9
10
|
export interface RecentMemoriesOptions {
|
|
10
11
|
/** Project container tag, from `getTags(directory).project.tag`. */
|
|
11
12
|
projectTag: string;
|
|
@@ -18,6 +19,8 @@ export interface RecentMemoriesOptions {
|
|
|
18
19
|
excludeCurrentSession?: boolean;
|
|
19
20
|
/** `CONFIG.chatMessage.maxAgeDays` */
|
|
20
21
|
maxAgeDays?: number;
|
|
22
|
+
/** Shared packing budget; hosts pass the snapshotted `retrievalMaxTokens`. */
|
|
23
|
+
budget?: ContextBudgetOptions;
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
23
26
|
* The project's most recent memories as a context section, shared by every host
|
|
@@ -27,7 +30,9 @@ export interface RecentMemoriesOptions {
|
|
|
27
30
|
export declare function buildRecentMemoriesSection(options: RecentMemoriesOptions): Promise<string | null>;
|
|
28
31
|
/** Delimit a retrieval section so it is recognisable as injected memory, not user text. */
|
|
29
32
|
export declare function wrapRetrievalSection(section: string): string;
|
|
33
|
+
/** Byte overhead the `omms-retrieval` wrapper adds around one section. */
|
|
34
|
+
export declare function retrievalWrapperBytes(): number;
|
|
30
35
|
/** Remove injected retrieval sections, so injected memories are never captured as user text. */
|
|
31
36
|
export declare function stripRetrievalSections(text: string): string;
|
|
32
|
-
/** Format a session's own memories for re-injection after compaction. */
|
|
33
|
-
export declare function formatMemoriesForCompaction(memories: any[]): string;
|
|
37
|
+
/** Format a session's own memories for re-injection after compaction, within the budget. */
|
|
38
|
+
export declare function formatMemoriesForCompaction(memories: any[], budget?: ContextBudgetOptions): string;
|