om-memory-system 3.2.0-next.9 → 3.3.0-next.18
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/README.md +3 -5
- package/dist/adapters/opencode/auto-capture-summary.js +50 -8
- package/dist/adapters/opencode/backfill-models.d.ts +25 -0
- package/dist/adapters/opencode/backfill-models.js +39 -0
- package/dist/adapters/opencode/backfill-startup.d.ts +7 -0
- package/dist/adapters/opencode/backfill-startup.js +12 -0
- package/dist/adapters/opencode/import-command.d.ts +3 -7
- package/dist/adapters/opencode/import-command.js +6 -54
- package/dist/adapters/pi/backfill-models.d.ts +9 -0
- package/dist/adapters/pi/backfill-models.js +30 -0
- package/dist/adapters/pi/extension.js +53 -0
- package/dist/adapters/pi/import-command.js +3 -0
- package/dist/adapters/pi/profile.js +2 -1
- package/dist/adapters/pi/provider.js +41 -14
- package/dist/cli/index.js +13 -1
- package/dist/cli/web-command.d.ts +3 -0
- package/dist/cli/web-command.js +72 -0
- package/dist/config.d.ts +30 -1
- package/dist/config.js +131 -9
- package/dist/core/capture-context.js +1 -1
- package/dist/core/capture.d.ts +4 -0
- package/dist/core/capture.js +30 -0
- package/dist/core/extraction.d.ts +23 -1
- package/dist/core/extraction.js +41 -0
- package/dist/core/host.d.ts +23 -0
- package/dist/core/host.js +9 -1
- package/dist/core/profile-analysis.js +9 -1
- package/dist/importer/auto-backfill.d.ts +20 -0
- package/dist/importer/auto-backfill.js +165 -0
- package/dist/importer/backfill-lock.d.ts +3 -0
- package/dist/importer/backfill-lock.js +44 -0
- package/dist/importer/backfill-model.d.ts +10 -0
- package/dist/importer/backfill-model.js +13 -0
- package/dist/importer/discovery.d.ts +32 -5
- package/dist/importer/discovery.js +88 -36
- package/dist/importer/import-args.d.ts +23 -1
- package/dist/importer/import-args.js +29 -1
- package/dist/importer/import-project.d.ts +16 -0
- package/dist/importer/import-project.js +35 -0
- package/dist/importer/import-readiness.d.ts +47 -0
- package/dist/importer/import-readiness.js +54 -0
- package/dist/importer/import-sessions.d.ts +98 -0
- package/dist/importer/import-sessions.js +222 -0
- package/dist/importer/import-sources.d.ts +47 -0
- package/dist/importer/import-sources.js +147 -0
- package/dist/importer/importer.d.ts +31 -1
- package/dist/importer/importer.js +96 -24
- package/dist/importer/manual-import-guard.d.ts +4 -0
- package/dist/importer/manual-import-guard.js +9 -0
- package/dist/importer/opencode-import.d.ts +11 -1
- package/dist/importer/opencode-import.js +133 -69
- package/dist/importer/opencode-project.d.ts +1 -4
- package/dist/importer/opencode-project.js +3 -36
- package/dist/importer/opencode-reader.d.ts +25 -5
- package/dist/importer/opencode-reader.js +92 -106
- package/dist/importer/opencode-snapshot.d.ts +82 -0
- package/dist/importer/opencode-snapshot.js +276 -0
- package/dist/importer/profile-import.d.ts +1 -0
- package/dist/importer/profile-import.js +7 -1
- package/dist/importer/run-import.d.ts +10 -0
- package/dist/importer/run-import.js +21 -0
- package/dist/importer/settings-health.d.ts +24 -0
- package/dist/importer/settings-health.js +127 -0
- package/dist/importer/web-import-api.d.ts +8 -0
- package/dist/importer/web-import-api.js +8 -0
- package/dist/importer/web-import-jobs.d.ts +53 -0
- package/dist/importer/web-import-jobs.js +227 -0
- package/dist/index.js +34 -0
- package/dist/services/ai/live-model-choice.js +6 -3
- package/dist/services/ai/opencode-import-models.d.ts +11 -0
- package/dist/services/ai/opencode-import-models.js +55 -0
- package/dist/services/ai/opencode-provider.d.ts +9 -0
- package/dist/services/ai/opencode-provider.js +31 -4
- package/dist/services/ai/providers/anthropic-messages.js +14 -6
- package/dist/services/ai/providers/base-provider.d.ts +8 -0
- package/dist/services/ai/providers/base-provider.js +12 -0
- package/dist/services/ai/providers/google-gemini.js +21 -7
- package/dist/services/ai/providers/openai-chat-completion.js +20 -7
- package/dist/services/ai/providers/openai-responses.js +11 -0
- package/dist/services/backfill-state.d.ts +25 -0
- package/dist/services/backfill-state.js +68 -0
- package/dist/services/capture-attempt-store.d.ts +21 -0
- package/dist/services/capture-attempt-store.js +108 -0
- package/dist/services/capture-diagnostics.d.ts +66 -0
- package/dist/services/capture-diagnostics.js +175 -0
- package/dist/services/cleanup-service.js +9 -0
- package/dist/services/global-config-writer.d.ts +10 -0
- package/dist/services/global-config-writer.js +110 -0
- package/dist/services/live-captured-entries.d.ts +3 -0
- package/dist/services/live-captured-entries.js +34 -0
- package/dist/services/log-path.d.ts +2 -0
- package/dist/services/log-path.js +15 -0
- package/dist/services/logger.js +1 -12
- package/dist/services/safe-health-error.d.ts +2 -0
- package/dist/services/safe-health-error.js +11 -0
- package/dist/services/settings-log.d.ts +5 -0
- package/dist/services/settings-log.js +36 -0
- package/dist/services/settings-models.d.ts +35 -0
- package/dist/services/settings-models.js +39 -0
- package/dist/services/settings-snapshot.d.ts +24 -0
- package/dist/services/settings-snapshot.js +83 -0
- package/dist/services/settings-traces.d.ts +7 -0
- package/dist/services/settings-traces.js +39 -0
- package/dist/services/user-memory-learning.js +2 -1
- package/dist/services/web-autostart.d.ts +29 -0
- package/dist/services/web-autostart.js +208 -0
- package/dist/services/web-server.d.ts +5 -0
- package/dist/services/web-server.js +187 -3
- package/dist/v2/legacy-client.js +19 -2
- package/dist/web/assets/index-B-_gMSn9.js +124 -0
- package/dist/web/assets/index-D5ak5KsI.css +2 -0
- package/dist/web/index.html +2 -2
- package/package.json +2 -1
- package/dist/web/assets/index-DPjEpD1h.css +0 -2
- package/dist/web/assets/index-do26c4JJ.js +0 -122
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { homedir } from "node:os";
|
|
2
|
+
/** Run the standalone web app or manage its per-user login item. */
|
|
3
|
+
export async function runWebCommand(args, options = {}, online) {
|
|
4
|
+
const home = options.home ?? homedir();
|
|
5
|
+
const config = await import("../config.js");
|
|
6
|
+
config.initConfig(home);
|
|
7
|
+
const { installWebAutostart, removeWebAutostart, webAutostartStatus } = await import("../services/web-autostart.js");
|
|
8
|
+
const [action, ...rest] = args;
|
|
9
|
+
if (rest.length ||
|
|
10
|
+
(action && !["install", "uninstall", "status", "--login-item"].includes(action))) {
|
|
11
|
+
console.error("Usage: om-memory-system web [install|uninstall|status]");
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
14
|
+
if (action === "install" && !config.CONFIG.webServerEnabled) {
|
|
15
|
+
console.error("OMMS web server is disabled (webServerEnabled is false)");
|
|
16
|
+
return 1;
|
|
17
|
+
}
|
|
18
|
+
if (action === "install" || action === "uninstall") {
|
|
19
|
+
const { readGlobalConfigRevision, writeGlobalConfigKeys } = await import("../services/global-config-writer.js");
|
|
20
|
+
await writeGlobalConfigKeys({ webServerAutoStart: action === "install" }, readGlobalConfigRevision());
|
|
21
|
+
config.initConfig(home);
|
|
22
|
+
const result = action === "install"
|
|
23
|
+
? installWebAutostart({ ...options, start: true })
|
|
24
|
+
: removeWebAutostart({ ...options, start: true });
|
|
25
|
+
console.log(`OMMS login item: ${result.state}`);
|
|
26
|
+
return action === "install" && result.state !== "installed" ? 1 : 0;
|
|
27
|
+
}
|
|
28
|
+
if (action === "status") {
|
|
29
|
+
const status = webAutostartStatus(options);
|
|
30
|
+
const check = online ??
|
|
31
|
+
(async () => {
|
|
32
|
+
const { WebServer } = await import("../services/web-server.js");
|
|
33
|
+
return new WebServer({
|
|
34
|
+
port: config.CONFIG.webServerPort,
|
|
35
|
+
host: config.CONFIG.webServerHost,
|
|
36
|
+
enabled: true,
|
|
37
|
+
apiToken: config.CONFIG.webServerApiToken,
|
|
38
|
+
}).checkServerAvailable();
|
|
39
|
+
});
|
|
40
|
+
console.log(JSON.stringify({ setting: config.CONFIG.webServerAutoStart, item: status, online: await check() }, null, 2));
|
|
41
|
+
return 0;
|
|
42
|
+
}
|
|
43
|
+
if (!config.CONFIG.webServerEnabled) {
|
|
44
|
+
console.error("OMMS web server is disabled (webServerEnabled is false)");
|
|
45
|
+
return 1;
|
|
46
|
+
}
|
|
47
|
+
const { WebAuth } = await import("../services/web-auth.js");
|
|
48
|
+
const { startWebServer } = await import("../services/web-server.js");
|
|
49
|
+
const server = await startWebServer({
|
|
50
|
+
directory: home,
|
|
51
|
+
port: config.CONFIG.webServerPort,
|
|
52
|
+
host: config.CONFIG.webServerHost,
|
|
53
|
+
enabled: true,
|
|
54
|
+
apiToken: config.CONFIG.webServerApiToken,
|
|
55
|
+
auth: new WebAuth({
|
|
56
|
+
password: config.CONFIG.webServerAuthPassword,
|
|
57
|
+
username: config.CONFIG.webServerAuthUsername,
|
|
58
|
+
}),
|
|
59
|
+
});
|
|
60
|
+
console.log(`OMMS web app: ${server.getUrl()}`);
|
|
61
|
+
// The Node HTTP adapter unrefs its socket for plugin use. Keep the CLI alive.
|
|
62
|
+
const keeper = setInterval(() => { }, 60_000);
|
|
63
|
+
const stop = () => {
|
|
64
|
+
clearInterval(keeper);
|
|
65
|
+
process.off("SIGINT", stop);
|
|
66
|
+
process.off("SIGTERM", stop);
|
|
67
|
+
void server.stop();
|
|
68
|
+
};
|
|
69
|
+
process.once("SIGINT", stop);
|
|
70
|
+
process.once("SIGTERM", stop);
|
|
71
|
+
return 0;
|
|
72
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -38,6 +38,14 @@ interface OmmsConfig {
|
|
|
38
38
|
piProvider?: string;
|
|
39
39
|
piModel?: string;
|
|
40
40
|
aiSessionRetentionDays?: number;
|
|
41
|
+
/** Write full capture prompts and replies to ~/.omms/traces. Global config only. */
|
|
42
|
+
captureTrace?: boolean;
|
|
43
|
+
captureTraceRetentionDays?: number;
|
|
44
|
+
captureAttemptRetentionDays?: number;
|
|
45
|
+
autoBackfill?: boolean;
|
|
46
|
+
opencodeBackfillModel?: string;
|
|
47
|
+
piBackfillModel?: string;
|
|
48
|
+
webServerAutoStart?: boolean;
|
|
41
49
|
webServerEnabled?: boolean;
|
|
42
50
|
webServerPort?: number;
|
|
43
51
|
webServerHost?: string;
|
|
@@ -85,6 +93,7 @@ interface OmmsConfig {
|
|
|
85
93
|
injectOn?: "first" | "always";
|
|
86
94
|
};
|
|
87
95
|
}
|
|
96
|
+
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 BOTH embeddingApiUrl and embeddingApiKey (OpenAI-compatible /embeddings).\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 (both URL and key required)\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 \"provider/model\"\n // \"opencodeBackfillModel\": \"inherit\", // or \"provider/model\"\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 // Required when webServerHost is not loopback. Protects /api/* with Bearer / X-Opencode-Mem-Token.\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.\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\"\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 // Maximum number of times to retry capturing a prompt if it fails (due to network, API errors, etc.)\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 // 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";
|
|
88
97
|
export declare function normalizeAutoCaptureMaxContextBytes(value: number): number;
|
|
89
98
|
export declare function normalizeAutoCleanupRetentionDays(value: number): number;
|
|
90
99
|
declare function buildConfig(fileConfig: OmmsConfig): {
|
|
@@ -119,6 +128,13 @@ declare function buildConfig(fileConfig: OmmsConfig): {
|
|
|
119
128
|
piModel: string | undefined;
|
|
120
129
|
autoCaptureProviderStatus: import("./config.js").AutoCaptureProviderStatus;
|
|
121
130
|
aiSessionRetentionDays: number;
|
|
131
|
+
captureTrace: boolean;
|
|
132
|
+
captureTraceRetentionDays: number;
|
|
133
|
+
captureAttemptRetentionDays: number;
|
|
134
|
+
autoBackfill: boolean;
|
|
135
|
+
opencodeBackfillModel: string;
|
|
136
|
+
piBackfillModel: string;
|
|
137
|
+
webServerAutoStart: boolean;
|
|
122
138
|
webServerEnabled: boolean;
|
|
123
139
|
webServerPort: number;
|
|
124
140
|
webServerHost: string;
|
|
@@ -169,6 +185,9 @@ declare function buildConfig(fileConfig: OmmsConfig): {
|
|
|
169
185
|
injectOn: "first" | "always";
|
|
170
186
|
};
|
|
171
187
|
};
|
|
188
|
+
export declare function getGlobalConfigSourcePath(): string | undefined;
|
|
189
|
+
export declare function getGlobalConfigWritePath(): string;
|
|
190
|
+
export declare function validateGlobalConfig(value: unknown): void;
|
|
172
191
|
export declare let CONFIG: {
|
|
173
192
|
storagePath: string;
|
|
174
193
|
userEmailOverride: string | undefined;
|
|
@@ -201,6 +220,13 @@ export declare let CONFIG: {
|
|
|
201
220
|
piModel: string | undefined;
|
|
202
221
|
autoCaptureProviderStatus: import("./config.js").AutoCaptureProviderStatus;
|
|
203
222
|
aiSessionRetentionDays: number;
|
|
223
|
+
captureTrace: boolean;
|
|
224
|
+
captureTraceRetentionDays: number;
|
|
225
|
+
captureAttemptRetentionDays: number;
|
|
226
|
+
autoBackfill: boolean;
|
|
227
|
+
opencodeBackfillModel: string;
|
|
228
|
+
piBackfillModel: string;
|
|
229
|
+
webServerAutoStart: boolean;
|
|
204
230
|
webServerEnabled: boolean;
|
|
205
231
|
webServerPort: number;
|
|
206
232
|
webServerHost: string;
|
|
@@ -255,7 +281,10 @@ type RuntimeConfig = ReturnType<typeof buildConfig>;
|
|
|
255
281
|
export { getAutoCaptureProviderStatus, type AutoCaptureProviderStatus, } from "./services/ai/live-model-choice.js";
|
|
256
282
|
export { isPlaceholderApiKey };
|
|
257
283
|
export declare function hasAutoCaptureProviderConfig(config?: RuntimeConfig): boolean;
|
|
258
|
-
export declare function
|
|
284
|
+
export declare function refreshConfigIfChanged(directory: string): void;
|
|
285
|
+
export declare function initConfig(directory: string, options?: {
|
|
286
|
+
strict?: boolean;
|
|
287
|
+
}): void;
|
|
259
288
|
/**
|
|
260
289
|
* initConfig plus the one-time legacy store migration (decision D13).
|
|
261
290
|
*
|
package/dist/config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync, mkdirSync, writeFileSync } from "node:fs";
|
|
1
|
+
import { existsSync, readFileSync, mkdirSync, writeFileSync, statSync } from "node:fs";
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
4
|
import { stripJsoncComments } from "./services/jsonc.js";
|
|
@@ -6,6 +6,7 @@ import { log } from "./services/logger.js";
|
|
|
6
6
|
import { resolveSecretValue } from "./services/secret-resolver.js";
|
|
7
7
|
import { isPlaceholderApiKey } from "./services/ai/api-key-placeholder.js";
|
|
8
8
|
import { getAutoCaptureProviderStatus } from "./services/ai/live-model-choice.js";
|
|
9
|
+
import { parseBackfillModel } from "./importer/backfill-model.js";
|
|
9
10
|
import { resolveDefaultStoragePath, runLegacyStoreMigration, legacyMigrationPaths, } from "./services/legacy-migration.js";
|
|
10
11
|
const OMMS_CONFIG_DIR = join(homedir(), ".config", "omms");
|
|
11
12
|
const DATA_DIR = join(homedir(), ".omms");
|
|
@@ -48,6 +49,13 @@ const DEFAULTS = {
|
|
|
48
49
|
autoCaptureMaxRetries: 3,
|
|
49
50
|
autoCaptureMaxContextBytes: 131072,
|
|
50
51
|
aiSessionRetentionDays: 7,
|
|
52
|
+
captureTrace: false,
|
|
53
|
+
captureTraceRetentionDays: 7,
|
|
54
|
+
captureAttemptRetentionDays: 30,
|
|
55
|
+
autoBackfill: true,
|
|
56
|
+
opencodeBackfillModel: "inherit",
|
|
57
|
+
piBackfillModel: "inherit",
|
|
58
|
+
webServerAutoStart: true,
|
|
51
59
|
webServerEnabled: true,
|
|
52
60
|
webServerPort: 4747,
|
|
53
61
|
webServerHost: "127.0.0.1",
|
|
@@ -108,8 +116,9 @@ function expandPath(path) {
|
|
|
108
116
|
* Load the first config file that exists, in priority order. A file that
|
|
109
117
|
* exists but cannot be read or parsed is not skipped: falling through would let
|
|
110
118
|
* a lower-priority (legacy) file silently supply settings such as storagePath.
|
|
119
|
+
* `strict` (live reload) throws instead, so running hosts keep their settings.
|
|
111
120
|
*/
|
|
112
|
-
function loadConfigFromPaths(paths) {
|
|
121
|
+
function loadConfigFromPaths(paths, strict = false) {
|
|
113
122
|
const path = paths.find((candidate) => existsSync(candidate));
|
|
114
123
|
if (!path)
|
|
115
124
|
return {};
|
|
@@ -119,6 +128,9 @@ function loadConfigFromPaths(paths) {
|
|
|
119
128
|
return JSON.parse(json);
|
|
120
129
|
}
|
|
121
130
|
catch (error) {
|
|
131
|
+
// The parser's message can quote file content, so a reload names the file only.
|
|
132
|
+
if (strict)
|
|
133
|
+
throw new Error(`Config file cannot be parsed: ${path}`, { cause: error });
|
|
122
134
|
log("Config file is invalid; using defaults instead of lower-priority files", {
|
|
123
135
|
path,
|
|
124
136
|
error: String(error),
|
|
@@ -140,7 +152,7 @@ function assertProjectRemoteProviderConfigIsSafe(projectConfig) {
|
|
|
140
152
|
`Move them to the global config at ${CONFIG_FILES[0]}.`);
|
|
141
153
|
}
|
|
142
154
|
}
|
|
143
|
-
const CONFIG_TEMPLATE = `{
|
|
155
|
+
export const CONFIG_TEMPLATE = `{
|
|
144
156
|
// ============================================
|
|
145
157
|
// omms (Opinionated Modular Memory System) Configuration
|
|
146
158
|
// ============================================
|
|
@@ -184,6 +196,13 @@ const CONFIG_TEMPLATE = `{
|
|
|
184
196
|
// Web Server Settings
|
|
185
197
|
// ============================================
|
|
186
198
|
|
|
199
|
+
// Start a background import of past chats on each host's next start.
|
|
200
|
+
"autoBackfill": true,
|
|
201
|
+
// "piBackfillModel": "inherit", // or "provider/model"
|
|
202
|
+
// "opencodeBackfillModel": "inherit", // or "provider/model"
|
|
203
|
+
|
|
204
|
+
// Register the web app to start when you log in.
|
|
205
|
+
"webServerAutoStart": true,
|
|
187
206
|
// Enable web UI for managing memories (accessible at http://localhost:4747)
|
|
188
207
|
"webServerEnabled": true,
|
|
189
208
|
|
|
@@ -352,6 +371,17 @@ const CONFIG_TEMPLATE = `{
|
|
|
352
371
|
// Days to keep AI session history before cleanup
|
|
353
372
|
"aiSessionRetentionDays": 7,
|
|
354
373
|
|
|
374
|
+
// Capture diagnostics: every capture attempt always writes one metadata line
|
|
375
|
+
// (model, stop reason, sizes, outcome) to ~/.omms/omms.log, with no
|
|
376
|
+
// conversation text. Set captureTrace to true to also write each attempt's
|
|
377
|
+
// prompt and raw reply to ~/.omms/traces/ for debugging. Traces can contain
|
|
378
|
+
// conversation content: <private> text and common API key formats are
|
|
379
|
+
// redacted, files are readable only by you, and they are deleted after
|
|
380
|
+
// captureTraceRetentionDays. Only this global file can turn tracing on.
|
|
381
|
+
// "captureTrace": false,
|
|
382
|
+
// "captureTraceRetentionDays": 7,
|
|
383
|
+
// "captureAttemptRetentionDays": 30,
|
|
384
|
+
|
|
355
385
|
// Temperature for AI API requests (set to false to omit parameter for models that don't support it)
|
|
356
386
|
// Some reasoning models (like o1, o3, gpt-5) don't support temperature parameter
|
|
357
387
|
// Set to false and add "memoryTemperature": false in config when using such models
|
|
@@ -530,6 +560,13 @@ function buildConfig(fileConfig) {
|
|
|
530
560
|
getEmbeddingDimensions(fileConfig.embeddingModel ?? DEFAULTS.embeddingModel);
|
|
531
561
|
const autoCaptureMaxContextBytes = normalizeAutoCaptureMaxContextBytes(fileConfig.autoCaptureMaxContextBytes ?? DEFAULTS.autoCaptureMaxContextBytes);
|
|
532
562
|
const userProfileAutoCleanupInterval = fileConfig.userProfileAutoCleanupInterval ?? DEFAULTS.userProfileAutoCleanupInterval;
|
|
563
|
+
parseBackfillModel(fileConfig, "pi");
|
|
564
|
+
parseBackfillModel(fileConfig, "opencode");
|
|
565
|
+
for (const key of ["autoBackfill", "webServerAutoStart"]) {
|
|
566
|
+
if (fileConfig[key] !== undefined && typeof fileConfig[key] !== "boolean") {
|
|
567
|
+
throw new Error(`Invalid ${key} config`);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
533
570
|
if (!Number.isInteger(embeddingDimensions) ||
|
|
534
571
|
embeddingDimensions <= 0 ||
|
|
535
572
|
embeddingDimensions > 65536) {
|
|
@@ -580,6 +617,13 @@ function buildConfig(fileConfig) {
|
|
|
580
617
|
memoryApiKey,
|
|
581
618
|
}),
|
|
582
619
|
aiSessionRetentionDays: fileConfig.aiSessionRetentionDays ?? DEFAULTS.aiSessionRetentionDays,
|
|
620
|
+
captureTrace: fileConfig.captureTrace === true,
|
|
621
|
+
captureTraceRetentionDays: Math.max(1, Math.floor(fileConfig.captureTraceRetentionDays ?? DEFAULTS.captureTraceRetentionDays)),
|
|
622
|
+
captureAttemptRetentionDays: normalizeAutoCleanupRetentionDays(fileConfig.captureAttemptRetentionDays ?? DEFAULTS.captureAttemptRetentionDays),
|
|
623
|
+
autoBackfill: fileConfig.autoBackfill ?? DEFAULTS.autoBackfill,
|
|
624
|
+
opencodeBackfillModel: fileConfig.opencodeBackfillModel ?? DEFAULTS.opencodeBackfillModel,
|
|
625
|
+
piBackfillModel: fileConfig.piBackfillModel ?? DEFAULTS.piBackfillModel,
|
|
626
|
+
webServerAutoStart: fileConfig.webServerAutoStart ?? DEFAULTS.webServerAutoStart,
|
|
583
627
|
webServerEnabled: fileConfig.webServerEnabled ?? DEFAULTS.webServerEnabled,
|
|
584
628
|
webServerPort: fileConfig.webServerPort ?? DEFAULTS.webServerPort,
|
|
585
629
|
webServerHost: fileConfig.webServerHost ?? DEFAULTS.webServerHost,
|
|
@@ -638,6 +682,21 @@ function buildConfig(fileConfig) {
|
|
|
638
682
|
},
|
|
639
683
|
};
|
|
640
684
|
}
|
|
685
|
+
export function getGlobalConfigSourcePath() {
|
|
686
|
+
return CONFIG_FILES.find((candidate) => existsSync(candidate));
|
|
687
|
+
}
|
|
688
|
+
export function getGlobalConfigWritePath() {
|
|
689
|
+
const source = getGlobalConfigSourcePath();
|
|
690
|
+
return source && !LEGACY_CONFIG_FILES.includes(source)
|
|
691
|
+
? source
|
|
692
|
+
: join(OMMS_CONFIG_DIR, "omms.jsonc");
|
|
693
|
+
}
|
|
694
|
+
export function validateGlobalConfig(value) {
|
|
695
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
696
|
+
throw new Error("Global config must be an object");
|
|
697
|
+
}
|
|
698
|
+
buildConfig(value);
|
|
699
|
+
}
|
|
641
700
|
const _globalFileConfig = loadConfigFromPaths(CONFIG_FILES);
|
|
642
701
|
let lastFileConfig = _globalFileConfig;
|
|
643
702
|
export let CONFIG = buildConfig(_globalFileConfig);
|
|
@@ -646,23 +705,86 @@ export { isPlaceholderApiKey };
|
|
|
646
705
|
export function hasAutoCaptureProviderConfig(config = CONFIG) {
|
|
647
706
|
return getAutoCaptureProviderStatus(config).ready;
|
|
648
707
|
}
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
const projectPaths = [
|
|
708
|
+
function projectConfigPaths(directory) {
|
|
709
|
+
return [
|
|
652
710
|
join(directory, ".opencode", "omms.jsonc"),
|
|
653
711
|
join(directory, ".opencode", "omms.json"),
|
|
654
712
|
join(directory, ".opencode", "opencode-mem.jsonc"),
|
|
655
713
|
join(directory, ".opencode", "opencode-mem.json"),
|
|
656
714
|
];
|
|
657
|
-
|
|
658
|
-
|
|
715
|
+
}
|
|
716
|
+
function configSignature(directory) {
|
|
717
|
+
return [...CONFIG_FILES, ...projectConfigPaths(directory)]
|
|
718
|
+
.filter((path) => existsSync(path))
|
|
719
|
+
.map((path) => {
|
|
720
|
+
try {
|
|
721
|
+
const stat = statSync(path);
|
|
722
|
+
return `${path}:${stat.mtimeMs}:${stat.size}`;
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
if (error.code === "ENOENT")
|
|
726
|
+
return `${path}:missing`;
|
|
727
|
+
throw error;
|
|
728
|
+
}
|
|
729
|
+
})
|
|
730
|
+
.join("|");
|
|
731
|
+
}
|
|
732
|
+
let lastConfigDirectory;
|
|
733
|
+
let lastConfigSignature;
|
|
734
|
+
/** Directory and signature of the last reload that failed. */
|
|
735
|
+
let lastFailedConfig;
|
|
736
|
+
export function refreshConfigIfChanged(directory) {
|
|
737
|
+
// Both hosts initialise their config at startup. Direct callers that have
|
|
738
|
+
// not initialised it keep their in-memory settings untouched.
|
|
739
|
+
if (lastConfigDirectory === undefined)
|
|
740
|
+
return;
|
|
741
|
+
let attempt;
|
|
742
|
+
try {
|
|
743
|
+
const signature = configSignature(directory);
|
|
744
|
+
if (directory === lastConfigDirectory && signature === lastConfigSignature)
|
|
745
|
+
return;
|
|
746
|
+
attempt = `${directory}\0${signature}`;
|
|
747
|
+
if (attempt === lastFailedConfig)
|
|
748
|
+
return;
|
|
749
|
+
initConfig(directory, { strict: true });
|
|
750
|
+
lastFailedConfig = undefined;
|
|
751
|
+
}
|
|
752
|
+
catch (error) {
|
|
753
|
+
// A bad hand edit must not stop capture: keep the last good settings, and
|
|
754
|
+
// skip this exact file state until it changes, instead of re-reading it on
|
|
755
|
+
// every unit. The last good directory and signature stay as they were.
|
|
756
|
+
log("Config reload failed; keeping the previous settings", {
|
|
757
|
+
error: error instanceof Error ? error.message : String(error),
|
|
758
|
+
});
|
|
759
|
+
lastFailedConfig = attempt;
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
export function initConfig(directory, options = {}) {
|
|
763
|
+
// omms project overrides win; the legacy opencode-mem file is still read.
|
|
764
|
+
const globalConfig = loadConfigFromPaths(CONFIG_FILES, options.strict);
|
|
765
|
+
const projectConfig = loadConfigFromPaths(projectConfigPaths(directory), options.strict);
|
|
659
766
|
assertProjectRemoteProviderConfigIsSafe(projectConfig);
|
|
660
767
|
const projectOverrides = { ...projectConfig };
|
|
661
768
|
delete projectOverrides.autoCleanupEnabled;
|
|
662
769
|
delete projectOverrides.autoCleanupRetentionDays;
|
|
770
|
+
// A checked-in project file must not start recording conversations; it may
|
|
771
|
+
// only turn tracing off for its project.
|
|
772
|
+
if (projectOverrides.captureTrace === true) {
|
|
773
|
+
log("Project config cannot turn on captureTrace; the value was ignored", { directory });
|
|
774
|
+
delete projectOverrides.captureTrace;
|
|
775
|
+
}
|
|
776
|
+
delete projectOverrides.captureTraceRetentionDays;
|
|
777
|
+
delete projectOverrides.autoBackfill;
|
|
778
|
+
delete projectOverrides.opencodeBackfillModel;
|
|
779
|
+
delete projectOverrides.piBackfillModel;
|
|
780
|
+
delete projectOverrides.webServerAutoStart;
|
|
781
|
+
delete projectOverrides.webServerEnabled;
|
|
663
782
|
const merged = { ...globalConfig, ...projectOverrides };
|
|
783
|
+
const nextConfig = buildConfig(merged);
|
|
664
784
|
lastFileConfig = merged;
|
|
665
|
-
CONFIG =
|
|
785
|
+
CONFIG = nextConfig;
|
|
786
|
+
lastConfigDirectory = directory;
|
|
787
|
+
lastConfigSignature = configSignature(directory);
|
|
666
788
|
}
|
|
667
789
|
/**
|
|
668
790
|
* Rebuild CONFIG from the last loaded file config. Used after the legacy
|
|
@@ -4,7 +4,7 @@ const DEFAULT_AUTO_CAPTURE_MAX_CONTEXT_BYTES = 131072;
|
|
|
4
4
|
const CONTEXT_TRUNCATION_MARKER = "\n[... truncated to autoCaptureMaxContextBytes ...]\n";
|
|
5
5
|
const SUMMARY_REQUEST_OVERHEAD_BYTES = 1024;
|
|
6
6
|
const SUMMARY_OUTPUT_RESERVE_BYTES = 16384;
|
|
7
|
-
const SUMMARY_ANALYSIS_SUFFIX = 'Analyze this conversation. If it contains technical work (code, bugs, features, decisions), create a concise summary and relevant tags. If it\'s non-technical (greetings, casual chat, incomplete requests),
|
|
7
|
+
const SUMMARY_ANALYSIS_SUFFIX = 'Analyze this conversation. If it contains technical work (code, bugs, features, decisions), create a concise summary and relevant tags. If it\'s non-technical (greetings, casual chat, incomplete requests), set "type" to "skip" with an empty "summary".';
|
|
8
8
|
function fitTextResponses(textResponses, maxBytes) {
|
|
9
9
|
if (textResponses.length === 0 || maxBytes <= 0)
|
|
10
10
|
return "";
|
package/dist/core/capture.d.ts
CHANGED
|
@@ -12,4 +12,8 @@ export type CaptureResult = {
|
|
|
12
12
|
status: "skipped";
|
|
13
13
|
type?: string;
|
|
14
14
|
};
|
|
15
|
+
/**
|
|
16
|
+
* Run one capture attempt and write exactly one diagnostics record for it,
|
|
17
|
+
* whether it is saved, skipped, or fails.
|
|
18
|
+
*/
|
|
15
19
|
export declare function captureConversation(workUnit: CaptureWorkUnit, provider: CaptureSummaryProvider): Promise<CaptureResult>;
|
package/dist/core/capture.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { CONFIG, refreshConfigIfChanged } from "../config.js";
|
|
2
|
+
import { buildCaptureAttemptRecord, emitCaptureAttempt } from "../services/capture-diagnostics.js";
|
|
1
3
|
import { memoryClient } from "../services/client.js";
|
|
2
4
|
import { getTags } from "../services/tags.js";
|
|
3
5
|
import { buildMarkdownContext, getAutoCaptureMarkdownBudget } from "./capture-context.js";
|
|
@@ -15,7 +17,30 @@ async function getLatestProjectMemory(containerTag) {
|
|
|
15
17
|
return null;
|
|
16
18
|
}
|
|
17
19
|
}
|
|
20
|
+
/**
|
|
21
|
+
* Run one capture attempt and write exactly one diagnostics record for it,
|
|
22
|
+
* whether it is saved, skipped, or fails.
|
|
23
|
+
*/
|
|
18
24
|
export async function captureConversation(workUnit, provider) {
|
|
25
|
+
refreshConfigIfChanged(workUnit.projectDirectory);
|
|
26
|
+
const diagnostics = {};
|
|
27
|
+
const startedAt = Date.now();
|
|
28
|
+
let outcome = "failed";
|
|
29
|
+
try {
|
|
30
|
+
const result = await runCapture(workUnit, provider, diagnostics);
|
|
31
|
+
outcome = result.status === "captured" ? "saved" : "skipped";
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
finally {
|
|
35
|
+
const record = buildCaptureAttemptRecord({
|
|
36
|
+
host: workUnit.host,
|
|
37
|
+
sourceType: workUnit.sourceType,
|
|
38
|
+
sessionId: workUnit.hostSessionId,
|
|
39
|
+
}, diagnostics, outcome, Date.now() - startedAt);
|
|
40
|
+
emitCaptureAttempt(record, diagnostics, CONFIG);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function runCapture(workUnit, provider, diagnostics) {
|
|
19
44
|
const tags = getTags(workUnit.projectDirectory);
|
|
20
45
|
const latestMemory = await getLatestProjectMemory(tags.project.tag);
|
|
21
46
|
const context = buildMarkdownContext(workUnit.userPrompt, workUnit.textResponses, workUnit.toolCalls, latestMemory, getAutoCaptureMarkdownBudget());
|
|
@@ -27,9 +52,11 @@ export async function captureConversation(workUnit, provider) {
|
|
|
27
52
|
projectDirectory: workUnit.projectDirectory,
|
|
28
53
|
userPrompt: workUnit.userPrompt,
|
|
29
54
|
prompt: workUnit.prompt,
|
|
55
|
+
diagnostics,
|
|
30
56
|
});
|
|
31
57
|
}
|
|
32
58
|
catch (error) {
|
|
59
|
+
diagnostics.failureReason ??= "call-error";
|
|
33
60
|
const message = error instanceof Error ? error.message : String(error);
|
|
34
61
|
throw new Error(`Summary generation failed: ${message}`, { cause: error });
|
|
35
62
|
}
|
|
@@ -40,6 +67,8 @@ export async function captureConversation(workUnit, provider) {
|
|
|
40
67
|
? `${summaryResult.summary}\n\nTags: ${summaryResult.tags.join(", ")}`
|
|
41
68
|
: summaryResult.summary;
|
|
42
69
|
const source = workUnit.sourceType === "history-import" ? "import" : "auto-capture";
|
|
70
|
+
// Anything that goes wrong from here on is a storage failure, thrown or reported.
|
|
71
|
+
diagnostics.failureReason = "persist-error";
|
|
43
72
|
const result = await memoryClient.addMemory(summaryWithTags, tags.project.tag, {
|
|
44
73
|
source,
|
|
45
74
|
type: summaryResult.type,
|
|
@@ -64,5 +93,6 @@ export async function captureConversation(workUnit, provider) {
|
|
|
64
93
|
if (!result.success) {
|
|
65
94
|
throw new Error(`Memory persistence failed: ${result.error || "database write failed"}`);
|
|
66
95
|
}
|
|
96
|
+
diagnostics.failureReason = undefined;
|
|
67
97
|
return { status: "captured", memoryId: result.id };
|
|
68
98
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import type { CaptureSummary } from "./host.js";
|
|
2
|
+
import type { CaptureFailureReason, CaptureSummary } from "./host.js";
|
|
3
3
|
/**
|
|
4
4
|
* Shared structured-extraction contract for automatic capture.
|
|
5
5
|
*
|
|
@@ -38,6 +38,13 @@ export declare const captureSummaryToolSchema: {
|
|
|
38
38
|
};
|
|
39
39
|
required: string[];
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Reply-format instruction for paths that get plain text back (the Pi model
|
|
43
|
+
* bridge). Structured-output and tool-call paths enforce the shape themselves,
|
|
44
|
+
* so without this the model only sees the Markdown layout of the summary field
|
|
45
|
+
* and `type="skip"`, and tends to answer in that form instead of JSON.
|
|
46
|
+
*/
|
|
47
|
+
export declare function buildCaptureReplyInstruction(): string;
|
|
41
48
|
export declare function buildCaptureSystemPrompt(languageName: string): string;
|
|
42
49
|
/** Upper bound for LLM-inferred preference confidence (0–1 scale). */
|
|
43
50
|
export declare const USER_PROFILE_LLM_CONFIDENCE_MAX = 1;
|
|
@@ -78,3 +85,18 @@ export declare function extractJsonObject(raw: string): unknown;
|
|
|
78
85
|
* blocks and surrounding prose. Returns null when no valid payload is present.
|
|
79
86
|
*/
|
|
80
87
|
export declare function parseCaptureSummary(raw: string): CaptureSummary | null;
|
|
88
|
+
/**
|
|
89
|
+
* Map provider-specific stop reasons onto one vocabulary so a length limit
|
|
90
|
+
* reads as `length` on every extraction path. Other values pass through in
|
|
91
|
+
* lower case.
|
|
92
|
+
*/
|
|
93
|
+
export declare function normalizeStopReason(stopReason: string | null | undefined): string | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Explain why a reply is not a usable capture summary. Returns null when the
|
|
96
|
+
* reply parses (including a skip). Codes are checked in the spec's order, so a
|
|
97
|
+
* cut-off reply reads as `truncated` rather than `invalid-json`.
|
|
98
|
+
*/
|
|
99
|
+
export declare function classifyCaptureReply(reply: {
|
|
100
|
+
text: string;
|
|
101
|
+
stopReason?: string | null;
|
|
102
|
+
}): CaptureFailureReason | null;
|
package/dist/core/extraction.js
CHANGED
|
@@ -40,6 +40,18 @@ export const captureSummaryToolSchema = {
|
|
|
40
40
|
},
|
|
41
41
|
required: ["summary", "type", "tags"],
|
|
42
42
|
};
|
|
43
|
+
/**
|
|
44
|
+
* Reply-format instruction for paths that get plain text back (the Pi model
|
|
45
|
+
* bridge). Structured-output and tool-call paths enforce the shape themselves,
|
|
46
|
+
* so without this the model only sees the Markdown layout of the summary field
|
|
47
|
+
* and `type="skip"`, and tends to answer in that form instead of JSON.
|
|
48
|
+
*/
|
|
49
|
+
export function buildCaptureReplyInstruction() {
|
|
50
|
+
return `Reply with only one JSON object and nothing else: no prose, no code fence. It must match this JSON schema:
|
|
51
|
+
${JSON.stringify(captureSummaryToolSchema)}
|
|
52
|
+
Put the Markdown summary (## Request / ## Outcome) inside the "summary" string.
|
|
53
|
+
For a non-technical conversation reply exactly: {"type":"skip","summary":"","tags":[]}`;
|
|
54
|
+
}
|
|
43
55
|
export function buildCaptureSystemPrompt(languageName) {
|
|
44
56
|
return `You are a technical memory recorder for a software development project.
|
|
45
57
|
|
|
@@ -143,3 +155,32 @@ export function parseCaptureSummary(raw) {
|
|
|
143
155
|
tags: parsed.data.tags.map((tag) => tag.toLowerCase().trim()).filter(Boolean),
|
|
144
156
|
};
|
|
145
157
|
}
|
|
158
|
+
const LENGTH_STOP_REASONS = new Set(["length", "max_tokens", "max_output_tokens"]);
|
|
159
|
+
/**
|
|
160
|
+
* Map provider-specific stop reasons onto one vocabulary so a length limit
|
|
161
|
+
* reads as `length` on every extraction path. Other values pass through in
|
|
162
|
+
* lower case.
|
|
163
|
+
*/
|
|
164
|
+
export function normalizeStopReason(stopReason) {
|
|
165
|
+
if (!stopReason)
|
|
166
|
+
return undefined;
|
|
167
|
+
const lower = stopReason.toLowerCase();
|
|
168
|
+
return LENGTH_STOP_REASONS.has(lower) ? "length" : lower;
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Explain why a reply is not a usable capture summary. Returns null when the
|
|
172
|
+
* reply parses (including a skip). Codes are checked in the spec's order, so a
|
|
173
|
+
* cut-off reply reads as `truncated` rather than `invalid-json`.
|
|
174
|
+
*/
|
|
175
|
+
export function classifyCaptureReply(reply) {
|
|
176
|
+
if (parseCaptureSummary(reply.text))
|
|
177
|
+
return null;
|
|
178
|
+
if (reply.text.trim().length === 0)
|
|
179
|
+
return "empty-text";
|
|
180
|
+
if (normalizeStopReason(reply.stopReason) === "length")
|
|
181
|
+
return "truncated";
|
|
182
|
+
const json = extractJsonObject(reply.text);
|
|
183
|
+
if (json === null || typeof json !== "object" || Array.isArray(json))
|
|
184
|
+
return "invalid-json";
|
|
185
|
+
return "schema-mismatch";
|
|
186
|
+
}
|
package/dist/core/host.d.ts
CHANGED
|
@@ -19,12 +19,35 @@ export interface CaptureSummary {
|
|
|
19
19
|
type: string;
|
|
20
20
|
tags: string[];
|
|
21
21
|
}
|
|
22
|
+
export type CaptureAttemptOutcome = "saved" | "skipped" | "failed";
|
|
23
|
+
/** Fixed failure codes, in the order classification checks them. */
|
|
24
|
+
export declare const CAPTURE_FAILURE_REASONS: readonly ["call-error", "empty-text", "truncated", "invalid-json", "schema-mismatch", "persist-error"];
|
|
25
|
+
export type CaptureFailureReason = (typeof CAPTURE_FAILURE_REASONS)[number];
|
|
26
|
+
export type CaptureExtractionPath = "host-model" | "external-api";
|
|
27
|
+
/**
|
|
28
|
+
* Filled in by the extraction path during one capture attempt. The capture
|
|
29
|
+
* pipeline owns the object and emits it once the outcome is known. Fields a
|
|
30
|
+
* path cannot observe stay undefined. The prompt and reply fields are only
|
|
31
|
+
* ever written to the opt-in trace file, never to the log.
|
|
32
|
+
*/
|
|
33
|
+
export interface CaptureAttemptDiagnostics {
|
|
34
|
+
path?: CaptureExtractionPath;
|
|
35
|
+
provider?: string;
|
|
36
|
+
model?: string;
|
|
37
|
+
stopReason?: string;
|
|
38
|
+
blockTypes?: string[];
|
|
39
|
+
systemPrompt?: string;
|
|
40
|
+
userPrompt?: string;
|
|
41
|
+
rawReply?: string;
|
|
42
|
+
failureReason?: CaptureFailureReason;
|
|
43
|
+
}
|
|
22
44
|
export interface CaptureSummaryRequest {
|
|
23
45
|
context: string;
|
|
24
46
|
sessionId: string;
|
|
25
47
|
projectDirectory: string;
|
|
26
48
|
userPrompt: string;
|
|
27
49
|
prompt?: CapturePromptContext;
|
|
50
|
+
diagnostics?: CaptureAttemptDiagnostics;
|
|
28
51
|
}
|
|
29
52
|
export interface CaptureSummaryProvider {
|
|
30
53
|
summarize(request: CaptureSummaryRequest): Promise<CaptureSummary | null>;
|
package/dist/core/host.js
CHANGED
|
@@ -10,7 +10,15 @@ CRITICAL: Detect the language used by the user in their prompts. You MUST output
|
|
|
10
10
|
|
|
11
11
|
CRITICAL: All JSON string values MUST escape double quotes with backslash. Do NOT use unescaped quotation marks inside string values.
|
|
12
12
|
|
|
13
|
-
Respond with
|
|
13
|
+
Respond with one JSON object. It must have these required fields:
|
|
14
|
+
{
|
|
15
|
+
"preferences": [{ "category": "string", "description": "string", "confidence": 0.8, "evidence": ["string"] }],
|
|
16
|
+
"patterns": [{ "category": "string", "description": "string" }],
|
|
17
|
+
"workflows": [{ "description": "string", "steps": ["string"] }]
|
|
18
|
+
}
|
|
19
|
+
Use an empty array for a field with no findings. Confidence must be a number from 0 to 1.
|
|
20
|
+
If you include "validations", each entry needs a numeric "index", a string "reason", and a "verdict" of "confirmed", "contradicted", "no_evidence", "inaccurate", or "oversimplified".
|
|
21
|
+
Return JSON only, without Markdown or other text.`;
|
|
14
22
|
}
|
|
15
23
|
/** Analyse prompts with a host-neutral model and merge the validated profile. */
|
|
16
24
|
export async function analyzeProfile(model, context, existingProfile, timeoutMs = 120000) {
|