om-memory-system 3.7.0-next.51 → 3.7.0-next.52

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.
Files changed (64) hide show
  1. package/dist/adapters/opencode/package-skills.d.ts +16 -0
  2. package/dist/adapters/opencode/package-skills.js +60 -0
  3. package/dist/adapters/opencode/profile-learning.js +9 -2
  4. package/dist/adapters/pi/extension.js +5 -1
  5. package/dist/adapters/pi/profile.js +7 -1
  6. package/dist/cli/index.js +5 -0
  7. package/dist/cli/profile-catch-up-command.d.ts +2 -0
  8. package/dist/cli/profile-catch-up-command.js +110 -0
  9. package/dist/cli/web-command.js +3 -6
  10. package/dist/config.d.ts +1 -1
  11. package/dist/config.js +6 -4
  12. package/dist/core/memory-tool-text.d.ts +4 -0
  13. package/dist/core/memory-tool-text.js +6 -0
  14. package/dist/core/profile-backoff.d.ts +20 -0
  15. package/dist/core/profile-backoff.js +41 -0
  16. package/dist/core/profile-failure.d.ts +16 -0
  17. package/dist/core/profile-failure.js +34 -0
  18. package/dist/core/trivial-prompt.d.ts +5 -0
  19. package/dist/core/trivial-prompt.js +8 -0
  20. package/dist/importer/claude-hook-api.js +13 -2
  21. package/dist/importer/model-selection.d.ts +2 -0
  22. package/dist/importer/model-selection.js +15 -4
  23. package/dist/importer/profile-backlog.d.ts +42 -0
  24. package/dist/importer/profile-backlog.js +79 -0
  25. package/dist/importer/profile-catch-up.d.ts +39 -0
  26. package/dist/importer/profile-catch-up.js +123 -0
  27. package/dist/importer/profile-import.d.ts +1 -1
  28. package/dist/importer/profile-import.js +14 -35
  29. package/dist/importer/settings-health.js +50 -11
  30. package/dist/importer/web-import-api.d.ts +1 -0
  31. package/dist/importer/web-import-api.js +1 -0
  32. package/dist/index.js +4 -1
  33. package/dist/services/api-tokens.d.ts +35 -0
  34. package/dist/services/api-tokens.js +185 -0
  35. package/dist/services/capture-attempt-store.d.ts +3 -1
  36. package/dist/services/capture-attempt-store.js +12 -5
  37. package/dist/services/context.js +4 -1
  38. package/dist/services/embedding-change.d.ts +52 -0
  39. package/dist/services/embedding-change.js +177 -0
  40. package/dist/services/embedding.d.ts +23 -1
  41. package/dist/services/embedding.js +63 -24
  42. package/dist/services/global-config-writer.d.ts +12 -1
  43. package/dist/services/global-config-writer.js +32 -4
  44. package/dist/services/migration-service.js +4 -1
  45. package/dist/services/settings-snapshot.d.ts +8 -0
  46. package/dist/services/settings-snapshot.js +11 -0
  47. package/dist/services/user-prompt/profile-catch-up-lease.d.ts +34 -0
  48. package/dist/services/user-prompt/profile-catch-up-lease.js +77 -0
  49. package/dist/services/user-prompt/user-prompt-manager.d.ts +14 -1
  50. package/dist/services/user-prompt/user-prompt-manager.js +36 -1
  51. package/dist/services/web-api-auth.d.ts +6 -2
  52. package/dist/services/web-api-auth.js +11 -9
  53. package/dist/services/web-password.d.ts +11 -0
  54. package/dist/services/web-password.js +30 -0
  55. package/dist/services/web-server.d.ts +8 -0
  56. package/dist/services/web-server.js +182 -19
  57. package/dist/v2/adapter.js +3 -0
  58. package/dist/web/assets/index-B6LyHM22.css +2 -0
  59. package/dist/web/assets/index-CVvXO3zL.js +125 -0
  60. package/dist/web/index.html +2 -2
  61. package/package.json +5 -1
  62. package/skills/omms-memory/SKILL.md +93 -0
  63. package/dist/web/assets/index-Bh5aFNPX.js +0 -124
  64. package/dist/web/assets/index-DG7RjW5B.css +0 -2
@@ -0,0 +1,16 @@
1
+ /**
2
+ * The package's `skills` folder: the nearest parent folder whose
3
+ * `package.json` names om-memory-system. Walking up also works when a host
4
+ * bundles the plugin into one file at another depth.
5
+ */
6
+ export declare function packageSkillsFolder(moduleUrl?: string): string;
7
+ /**
8
+ * V1: add the package's skills folder to OpenCode's skill paths once. The V1
9
+ * SDK `Config` type has no `skills` key, although OpenCode reads it.
10
+ */
11
+ export declare function applySkillsPathConfig(config: object, folder?: string): void;
12
+ /** V2: add the omms-memory skill to the plugin's skill list once. */
13
+ export declare function addPackageSkill(editor: {
14
+ get(id: string): unknown;
15
+ add(skill: any): void;
16
+ }, folder?: string): void;
@@ -0,0 +1,60 @@
1
+ import { existsSync, readFileSync } from "node:fs";
2
+ import { dirname, join } from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { log } from "../../services/logger.js";
5
+ const SKILL_NAME = "omms-memory";
6
+ /**
7
+ * The package's `skills` folder: the nearest parent folder whose
8
+ * `package.json` names om-memory-system. Walking up also works when a host
9
+ * bundles the plugin into one file at another depth.
10
+ */
11
+ export function packageSkillsFolder(moduleUrl = import.meta.url) {
12
+ let folder = dirname(fileURLToPath(moduleUrl));
13
+ for (let depth = 0; depth < 8; depth++) {
14
+ try {
15
+ const pkg = JSON.parse(readFileSync(join(folder, "package.json"), "utf8"));
16
+ if (pkg.name === "om-memory-system")
17
+ return join(folder, "skills");
18
+ }
19
+ catch {
20
+ // No readable package.json here; keep walking up.
21
+ }
22
+ const parent = dirname(folder);
23
+ if (parent === folder)
24
+ break;
25
+ folder = parent;
26
+ }
27
+ return join(dirname(fileURLToPath(moduleUrl)), "..", "..", "..", "skills");
28
+ }
29
+ const defaultFolder = packageSkillsFolder();
30
+ /**
31
+ * V1: add the package's skills folder to OpenCode's skill paths once. The V1
32
+ * SDK `Config` type has no `skills` key, although OpenCode reads it.
33
+ */
34
+ export function applySkillsPathConfig(config, folder = defaultFolder) {
35
+ const cfg = config;
36
+ if (!existsSync(folder)) {
37
+ log("OMMS skills folder not found; the omms-memory skill is not loaded", { folder });
38
+ return;
39
+ }
40
+ const paths = cfg.skills?.paths ?? [];
41
+ if (paths.includes(folder))
42
+ return;
43
+ cfg.skills = { ...cfg.skills, paths: [...paths, folder] };
44
+ }
45
+ /** V2: add the omms-memory skill to the plugin's skill list once. */
46
+ export function addPackageSkill(editor, folder = defaultFolder) {
47
+ if (editor.get(SKILL_NAME))
48
+ return;
49
+ const path = join(folder, SKILL_NAME, "SKILL.md");
50
+ let content;
51
+ try {
52
+ content = readFileSync(path, "utf8");
53
+ }
54
+ catch {
55
+ log("OMMS skill file not found; the omms-memory skill is not loaded", { path });
56
+ return;
57
+ }
58
+ const description = content.match(/^description: (.+)$/m)?.[1]?.trim();
59
+ editor.add({ id: SKILL_NAME, name: SKILL_NAME, description, path, content });
60
+ }
@@ -1,4 +1,5 @@
1
1
  import { getTags } from "../../services/tags.js";
2
+ import { profileBackoff, recordProfileFailure } from "../../core/profile-backoff.js";
2
3
  import { log } from "../../services/logger.js";
3
4
  import { CONFIG, refreshConfigIfChanged } from "../../config.js";
4
5
  import { resolveOpencodeHostModel } from "../../services/ai/live-model-choice.js";
@@ -42,6 +43,8 @@ async function runAutomaticProfileCleanup(userId) {
42
43
  export async function performUserProfileLearning(ctx, directory) {
43
44
  if (isLearningRunning)
44
45
  return;
46
+ if (!profileBackoff.canRun())
47
+ return;
45
48
  refreshConfigIfChanged(directory);
46
49
  if (!CONFIG.autoCaptureProviderStatus || !CONFIG.autoCaptureProviderStatus.ready) {
47
50
  log("user-profile-learning: skipped (provider not ready)", {
@@ -51,13 +54,16 @@ export async function performUserProfileLearning(ctx, directory) {
51
54
  }
52
55
  isLearningRunning = true;
53
56
  try {
57
+ await userPromptManager.skipTrivialPromptsForLearning();
54
58
  const count = await userPromptManager.countUnanalyzedForUserLearning();
55
59
  const threshold = CONFIG.userProfileAnalysisInterval;
56
60
  log("user-profile-learning: check", { count, threshold });
57
61
  if (count < threshold) {
58
62
  return;
59
63
  }
60
- const prompts = await userPromptManager.getPromptsForUserLearning(threshold);
64
+ const prompts = await userPromptManager.getPromptsForUserLearning(threshold, {
65
+ recentFirst: true,
66
+ });
61
67
  if (prompts.length === 0) {
62
68
  return;
63
69
  }
@@ -130,6 +136,7 @@ Rules:
130
136
  }
131
137
  const context = buildUserAnalysisContext(prompts, existingProfile, validationPrompt);
132
138
  const analysisResult = await analyzeUserProfile(context, existingProfile);
139
+ profileBackoff.recordSuccess();
133
140
  log("user-profile-learning: analyze done", { hasResult: !!analysisResult });
134
141
  if (!analysisResult) {
135
142
  await userPromptManager.markMultipleAsUserLearningCaptured(prompts.map((p) => p.id));
@@ -213,7 +220,7 @@ Rules:
213
220
  // would surface as an unhandled promise rejection. The caller (src/index.ts idle
214
221
  // timer) already wraps this call in its own try/catch, and issue #265 requires
215
222
  // provider errors to propagate instead of being masked, so rethrow after logging.
216
- log("user-profile-learning: aborted", { error: String(error) });
223
+ recordProfileFailure("opencode", "user-profile-learning: aborted", error);
217
224
  throw error;
218
225
  }
219
226
  finally {
@@ -2,6 +2,8 @@ import { Type } from "typebox";
2
2
  import { CONFIG, initConfigWithLegacyMigration, isConfigured, refreshConfigIfChanged, } from "../../config.js";
3
3
  import { pruneTraces } from "../../services/capture-diagnostics.js";
4
4
  import { executeMemoryOperation } from "../../core/memory-operations.js";
5
+ import { memoryToolDescription } from "../../core/memory-tool-text.js";
6
+ import { isTrivialPrompt } from "../../core/trivial-prompt.js";
5
7
  import { getLanguageName } from "../../services/language-detector.js";
6
8
  import { log } from "../../services/logger.js";
7
9
  import { memoryClient } from "../../services/client.js";
@@ -251,7 +253,9 @@ export default function ommsPiExtension(pi) {
251
253
  notify: notify(ctx),
252
254
  });
253
255
  const settledPrompt = lastSettledUserPrompt(entries);
256
+ // Pi batches this session's prompts in memory, so they are recent by design.
254
257
  if (settledPrompt &&
258
+ !isTrivialPrompt(settledPrompt) &&
255
259
  CONFIG.userProfileAnalysisInterval > 0 &&
256
260
  !memoryClient.getEmbeddingInitError?.()) {
257
261
  promptsSinceProfileAnalysis.push(settledPrompt);
@@ -295,7 +299,7 @@ export default function ommsPiExtension(pi) {
295
299
  pi.registerTool({
296
300
  name: "memory",
297
301
  label: "Memory",
298
- description: `Manage and query project memory (MATCH USER LANGUAGE: ${getLanguageName(CONFIG.autoCaptureLanguage || "en")}). Use 'search' with technical keywords/tags, 'add' to store knowledge, 'profile' for preferences. Use migrate/list-shards/export/import when a project directory moves. Search/list scope: project or all-projects.`,
302
+ description: memoryToolDescription(getLanguageName(CONFIG.autoCaptureLanguage || "en")),
299
303
  parameters: Type.Object({
300
304
  mode: Type.Optional(Type.Union(MEMORY_TOOL_MODES.map((mode) => Type.Literal(mode)))),
301
305
  content: Type.Optional(Type.String()),
@@ -1,5 +1,6 @@
1
1
  import { CONFIG, refreshConfigIfChanged } from "../../config.js";
2
2
  import { analyzeProfile, } from "../../core/profile-analysis.js";
3
+ import { profileBackoff, recordProfileFailure } from "../../core/profile-backoff.js";
3
4
  import { log } from "../../services/logger.js";
4
5
  import { getTags } from "../../services/tags.js";
5
6
  function replyText(reply) {
@@ -46,6 +47,10 @@ export function createPiProfileAnalyzer(resolveModel, timeoutMs = 120000) {
46
47
  export async function performPiProfileLearning(input) {
47
48
  if (input.prompts.length === 0)
48
49
  return;
50
+ if (!profileBackoff.canRun()) {
51
+ log("pi profile learning: skipped (waiting after a failure)");
52
+ return;
53
+ }
49
54
  try {
50
55
  refreshConfigIfChanged(input.directory);
51
56
  const tags = getTags(input.directory);
@@ -83,6 +88,7 @@ export async function performPiProfileLearning(input) {
83
88
  else {
84
89
  await userProfileManager.createProfile(userId, tags.user.displayName || userId, tags.user.userName || userId, tags.user.userEmail || userId, analysis.raw, input.prompts.length);
85
90
  }
91
+ profileBackoff.recordSuccess();
86
92
  log("pi profile learning: profile updated", {
87
93
  userId,
88
94
  prompts: input.prompts.length,
@@ -98,7 +104,7 @@ export async function performPiProfileLearning(input) {
98
104
  }
99
105
  catch (error) {
100
106
  const message = error instanceof Error ? error.message : String(error);
101
- log(`pi profile learning: aborted (${message})`);
107
+ recordProfileFailure("pi", "pi profile learning: aborted", error);
102
108
  if (CONFIG.showErrorToasts && input.notify) {
103
109
  const shortReason = message.length > 100 ? message.substring(0, 100) + "..." : message;
104
110
  await Promise.resolve(input.notify({
package/dist/cli/index.js CHANGED
@@ -16,6 +16,7 @@ Commands:
16
16
  web Start the web app in the foreground
17
17
  web install|uninstall|status Manage the web app login item
18
18
  memory <mode> [options] Search, add, list, or forget memories (run with --help)
19
+ profile-catch-up Analyse every prompt waiting for profile learning (run with --help)
19
20
  claude-hook <event> Run a Claude Code hook (used by the Claude Code plugin)
20
21
 
21
22
  Options:
@@ -56,6 +57,10 @@ export async function runCli(argv) {
56
57
  const { runMemoryCommand } = await import("./memory-command.js");
57
58
  return await runMemoryCommand(argv.slice(1));
58
59
  }
60
+ if (argv[0] === "profile-catch-up") {
61
+ const { runProfileCatchUpCommand } = await import("./profile-catch-up-command.js");
62
+ return await runProfileCatchUpCommand(argv.slice(1));
63
+ }
59
64
  if (argv[0] === "web") {
60
65
  try {
61
66
  const { runWebCommand } = await import("./web-command.js");
@@ -0,0 +1,2 @@
1
+ /** `om-memory-system profile-catch-up`: the same run as the Settings page button. */
2
+ export declare function runProfileCatchUpCommand(argv: string[], print?: (line: string) => void): Promise<number>;
@@ -0,0 +1,110 @@
1
+ const usage = `Usage: om-memory-system profile-catch-up [options]
2
+
3
+ Analyse every prompt that waits for profile learning, in batches of 50.
4
+ Without --yes the command prints the counts and exits without model calls.
5
+
6
+ Options:
7
+ --yes Start the run
8
+ --dry-run Print the counts only
9
+ --provider <type> Provider type (default: saved memoryProvider)
10
+ --model <id> Model id (default: saved memoryModel)
11
+ --api-url <url> API URL (default: saved memoryApiUrl)
12
+ --api-key-env <NAME> Read the API key from this environment variable
13
+ --help Show this help`;
14
+ const VALUE_FLAGS = {
15
+ "--provider": "provider",
16
+ "--model": "model",
17
+ "--api-url": "apiUrl",
18
+ "--api-key-env": "apiKeyEnv",
19
+ };
20
+ function parseFlags(argv) {
21
+ const flags = { yes: false, dryRun: false, help: false, model: {} };
22
+ for (let index = 0; index < argv.length; index++) {
23
+ const [name, inline] = argv[index].split(/=(.*)/s, 2);
24
+ if (name === "--yes")
25
+ flags.yes = true;
26
+ else if (name === "--dry-run")
27
+ flags.dryRun = true;
28
+ else if (name === "--help" || name === "-h")
29
+ flags.help = true;
30
+ else if (name in VALUE_FLAGS) {
31
+ const value = inline ?? argv[++index];
32
+ if (!value)
33
+ throw new Error(`${name} needs a value`);
34
+ flags.model[VALUE_FLAGS[name]] = value;
35
+ }
36
+ else
37
+ throw new Error(`Unknown option ${name}; use --help`);
38
+ }
39
+ return flags;
40
+ }
41
+ /** `om-memory-system profile-catch-up`: the same run as the Settings page button. */
42
+ export async function runProfileCatchUpCommand(argv, print = console.log) {
43
+ let flags;
44
+ try {
45
+ flags = parseFlags(argv);
46
+ }
47
+ catch (error) {
48
+ print(error instanceof Error ? error.message : String(error));
49
+ return 1;
50
+ }
51
+ if (flags.help) {
52
+ print(usage);
53
+ return 0;
54
+ }
55
+ const { CONFIG, initConfig, initConfigWithLegacyMigration } = await import("../config.js");
56
+ try {
57
+ if (flags.dryRun || !flags.yes)
58
+ initConfig(process.cwd());
59
+ else
60
+ initConfigWithLegacyMigration(process.cwd());
61
+ const { previewCatchUp, CATCH_UP_BATCH_SIZE } = await import("../importer/profile-catch-up.js");
62
+ const preview = await previewCatchUp();
63
+ print(`Waiting prompts: ${preview.waiting}`);
64
+ print(`Model calls: ${preview.calls} (batches of ${CATCH_UP_BATCH_SIZE})`);
65
+ if (flags.dryRun)
66
+ return 0;
67
+ if (!flags.yes) {
68
+ print("Run again with --yes to start.");
69
+ return 0;
70
+ }
71
+ const { getTags } = await import("../services/tags.js");
72
+ const user = getTags(process.cwd()).user;
73
+ if (!user.userEmail)
74
+ throw new Error("Profile learning needs a user email");
75
+ const { selectImportModel } = await import("../importer/model-selection.js");
76
+ const selected = selectImportModel(flags.model);
77
+ print(`Profile model: ${selected.provider}/${selected.modelId}`);
78
+ const { drainProfileBacklog } = await import("../importer/profile-backlog.js");
79
+ const { catchUpLeaseHooks } = await import("../importer/profile-catch-up.js");
80
+ const hooks = await catchUpLeaseHooks();
81
+ const report = await drainProfileBacklog({
82
+ beforeBatch: hooks.beforeBatch,
83
+ afterBatch: hooks.afterBatch,
84
+ user: { ...user, userEmail: user.userEmail },
85
+ model: selected.profile,
86
+ batchSize: CATCH_UP_BATCH_SIZE,
87
+ onProgress: ({ batchesBuilt, remaining }) => print(`Batches done: ${batchesBuilt}; prompts waiting: ${remaining}`),
88
+ }).finally(() => hooks.release().catch(() => { }));
89
+ print(`Batches done: ${report.batchesBuilt}; prompts waiting: ${report.remaining}`);
90
+ if (report.superseded) {
91
+ print("Stopped: a newer catch-up run took over.");
92
+ return 0;
93
+ }
94
+ if (report.reason) {
95
+ print(`Stopped: ${report.reason}. Run the command again to continue.`);
96
+ return 1;
97
+ }
98
+ return 0;
99
+ }
100
+ catch (error) {
101
+ const key = flags.model.apiKeyEnv ? process.env[flags.model.apiKeyEnv] : undefined;
102
+ let message = error instanceof Error ? error.message : String(error);
103
+ for (const secret of [key, CONFIG.memoryApiKey]) {
104
+ if (secret)
105
+ message = message.replaceAll(secret, "[redacted]");
106
+ }
107
+ print(`Profile catch-up failed: ${message}`);
108
+ return 1;
109
+ }
110
+ }
@@ -117,16 +117,13 @@ export async function runWebCommand(args, options = {}, online, handover = {}) {
117
117
  import("../services/auth-token.js"),
118
118
  import("../services/package-version.js"),
119
119
  ]);
120
- const apiToken = config.CONFIG.webServerApiToken;
121
120
  context = {
122
121
  fetchFn: handover.fetch ?? fetch,
123
122
  sleep: handover.sleep ?? ((ms) => new Promise((resolve) => setTimeout(resolve, ms))),
124
123
  url,
125
124
  port: config.CONFIG.webServerPort,
126
- headers: {
127
- [AUTH_HEADER]: getOrCreateAuthToken(),
128
- ...(apiToken ? { authorization: `Bearer ${apiToken}` } : {}),
129
- },
125
+ // The web app runs on this machine, so the local token file is enough.
126
+ headers: { [AUTH_HEADER]: getOrCreateAuthToken() },
130
127
  version: handover.version ?? packageVersion(),
131
128
  };
132
129
  outcome = await negotiateOwner(context);
@@ -148,7 +145,6 @@ export async function runWebCommand(args, options = {}, online, handover = {}) {
148
145
  port: config.CONFIG.webServerPort,
149
146
  host: config.CONFIG.webServerHost,
150
147
  enabled: true,
151
- apiToken: config.CONFIG.webServerApiToken,
152
148
  }).checkServerAvailable();
153
149
  });
154
150
  console.log(JSON.stringify({ setting: config.CONFIG.webServerAutoStart, item: status, url, online: await check() }, null, 2));
@@ -165,6 +161,7 @@ export async function runWebCommand(args, options = {}, online, handover = {}) {
165
161
  port: config.CONFIG.webServerPort,
166
162
  host: config.CONFIG.webServerHost,
167
163
  enabled: true,
164
+ // Imported into the token table once at start; not read for authorisation.
168
165
  apiToken: config.CONFIG.webServerApiToken,
169
166
  auth: new WebAuth({
170
167
  password: config.CONFIG.webServerAuthPassword,
package/dist/config.d.ts CHANGED
@@ -102,7 +102,7 @@ interface OmmsConfig {
102
102
  injectOn?: "first" | "always";
103
103
  };
104
104
  }
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 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 \"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 // Required when webServerHost is not loopback. Protects /api/* with Bearer / X-Omms-Token (legacy X-Opencode-Mem-Token still works).\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";
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";
106
106
  export declare function normalizeAutoCaptureMaxContextBytes(value: number): number;
107
107
  /** Whole hours from 0 to 720. A value that is not a number gives the default. */
108
108
  export declare function normalizeCaptureRetryRetentionHours(value: unknown): number;
package/dist/config.js CHANGED
@@ -174,7 +174,7 @@ export const CONFIG_TEMPLATE = `{
174
174
  // Embedding Model (for similarity search)
175
175
  // ============================================
176
176
  // Local = Hugging Face / ONNX via @huggingface/transformers (not Apple MLX).
177
- // Remote = set BOTH embeddingApiUrl and embeddingApiKey (OpenAI-compatible /embeddings).
177
+ // Remote = set embeddingApiUrl (OpenAI-compatible /embeddings); embeddingApiKey is optional.
178
178
 
179
179
  // Default: Nomic Embed v1 (768 dimensions, 8192 context, multilingual)
180
180
  "embeddingModel": "Xenova/nomic-embed-text-v1",
@@ -192,7 +192,7 @@ export const CONFIG_TEMPLATE = `{
192
192
  // "embeddingModel": "Xenova/all-MiniLM-L6-v2", // 384 dims, very fast, 512 context
193
193
  // "embeddingModel": "Xenova/all-mpnet-base-v2", // 768 dims, good quality, 512 context
194
194
 
195
- // Optional: OpenAI-compatible API for embeddings (both URL and key required)
195
+ // Optional: OpenAI-compatible API for embeddings (URL required; key only if the server needs one)
196
196
  // "embeddingApiUrl": "https://api.openai.com/v1",
197
197
  // "embeddingApiKey": "env://OPENAI_API_KEY", // or "sk-..." / "file:///path/to/key"
198
198
  // "embeddingModel": "text-embedding-3-small", // 1536 dims, auto-detected
@@ -225,7 +225,8 @@ export const CONFIG_TEMPLATE = `{
225
225
  // "webServerAuthPassword": "",
226
226
  // "webServerAuthUsername": "",
227
227
 
228
- // Required when webServerHost is not loopback. Protects /api/* with Bearer / X-Omms-Token (legacy X-Opencode-Mem-Token still works).
228
+ // Deprecated: imported once into the API token table, then no longer read.
229
+ // Create API tokens on the Settings page (Keys and access) instead.
229
230
  // "webServerApiToken": "env://OMMS_WEB_TOKEN",
230
231
 
231
232
  // ============================================
@@ -631,7 +632,8 @@ function buildConfig(fileConfig) {
631
632
  embeddingUseTaskPrefixes: fileConfig.embeddingUseTaskPrefixes ?? DEFAULTS.embeddingUseTaskPrefixes,
632
633
  embeddingApiUrl: fileConfig.embeddingApiUrl,
633
634
  embeddingApiKey: fileConfig.embeddingApiUrl
634
- ? resolveSecretValue(fileConfig.embeddingApiKey ?? process.env.OPENAI_API_KEY)
635
+ ? // No ambient OPENAI_API_KEY: an unset key means no Authorization header.
636
+ resolveSecretValue(fileConfig.embeddingApiKey)
635
637
  : undefined,
636
638
  similarityThreshold: fileConfig.similarityThreshold ?? DEFAULTS.similarityThreshold,
637
639
  maxMemories: fileConfig.maxMemories ?? DEFAULTS.maxMemories,
@@ -0,0 +1,4 @@
1
+ /** Earlier fixes and decisions are stored in memory, so agents should look there first. */
2
+ export declare const MEMORY_SEARCH_FIRST = "Search it before you debug or investigate a problem: earlier fixes and decisions are stored here.";
3
+ /** The `memory` tool description, the same text for Pi and OpenCode. */
4
+ export declare function memoryToolDescription(languageName: string): string;
@@ -0,0 +1,6 @@
1
+ /** Earlier fixes and decisions are stored in memory, so agents should look there first. */
2
+ export const MEMORY_SEARCH_FIRST = "Search it before you debug or investigate a problem: earlier fixes and decisions are stored here.";
3
+ /** The `memory` tool description, the same text for Pi and OpenCode. */
4
+ export function memoryToolDescription(languageName) {
5
+ return `Manage and query project memory (MATCH USER LANGUAGE: ${languageName}). ${MEMORY_SEARCH_FIRST} Use 'search' with technical keywords/tags, 'add' to store knowledge, 'profile' for preferences. Use migrate/list-shards/export/import when a project directory moves. Search/list scope: project or all-projects.`;
6
+ }
@@ -0,0 +1,20 @@
1
+ import { type ProfileFailureCode } from "./profile-failure.js";
2
+ /** After a failed profile pass, the process waits this long before the next one. */
3
+ export declare const PROFILE_FAILURE_WAIT_MS: number;
4
+ /**
5
+ * An in-process gate: a failing batch is not retried, and paid for, after
6
+ * every turn. Capture never checks it. A restart clears it.
7
+ */
8
+ export declare const profileBackoff: {
9
+ canRun(now?: number): boolean;
10
+ recordSuccess(): void;
11
+ recordFailure(now?: number): void;
12
+ };
13
+ /** Log one metadata record for a failed pass and start the wait. */
14
+ export declare function recordProfileFailure(host: "opencode" | "pi" | "claude-code", message: string, error: unknown, now?: number): ProfileFailureCode;
15
+ /** One catch-up run at a time, and no live pass in this process while it runs. */
16
+ export declare const profileCatchUpLock: {
17
+ isActive: () => boolean;
18
+ begin(): boolean;
19
+ end(): void;
20
+ };
@@ -0,0 +1,41 @@
1
+ import { log } from "../services/logger.js";
2
+ import { profileFailureCode } from "./profile-failure.js";
3
+ /** After a failed profile pass, the process waits this long before the next one. */
4
+ export const PROFILE_FAILURE_WAIT_MS = 10 * 60 * 1000;
5
+ let lastFailureAt = null;
6
+ /**
7
+ * An in-process gate: a failing batch is not retried, and paid for, after
8
+ * every turn. Capture never checks it. A restart clears it.
9
+ */
10
+ export const profileBackoff = {
11
+ canRun(now = Date.now()) {
12
+ return lastFailureAt === null || now - lastFailureAt >= PROFILE_FAILURE_WAIT_MS;
13
+ },
14
+ recordSuccess() {
15
+ lastFailureAt = null;
16
+ },
17
+ recordFailure(now = Date.now()) {
18
+ lastFailureAt = now;
19
+ },
20
+ };
21
+ /** Log one metadata record for a failed pass and start the wait. */
22
+ export function recordProfileFailure(host, message, error, now = Date.now()) {
23
+ const reason = profileFailureCode(error);
24
+ profileBackoff.recordFailure(now);
25
+ log(message, { host, reason });
26
+ return reason;
27
+ }
28
+ let catchUpActive = false;
29
+ /** One catch-up run at a time, and no live pass in this process while it runs. */
30
+ export const profileCatchUpLock = {
31
+ isActive: () => catchUpActive,
32
+ begin() {
33
+ if (catchUpActive)
34
+ return false;
35
+ catchUpActive = true;
36
+ return true;
37
+ },
38
+ end() {
39
+ catchUpActive = false;
40
+ },
41
+ };
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Fixed reason codes for a failed profile learning step. Log lines carry only
3
+ * the code, never the prompt, the reply, or the provider's message.
4
+ */
5
+ export type ProfileFailureCode = "timeout" | `http-${number}` | "no-tool-call" | "invalid-reply" | "not-configured" | "error";
6
+ export declare class ProfileModelError extends Error {
7
+ readonly code: ProfileFailureCode;
8
+ constructor(code: ProfileFailureCode);
9
+ }
10
+ /** The code for a failed provider call result. */
11
+ export declare function profileResultCode(result: {
12
+ error?: string;
13
+ httpStatus?: number;
14
+ }): ProfileFailureCode;
15
+ /** The code for any error thrown by a profile learning step. */
16
+ export declare function profileFailureCode(error: unknown): ProfileFailureCode;
@@ -0,0 +1,34 @@
1
+ export class ProfileModelError extends Error {
2
+ code;
3
+ constructor(code) {
4
+ super(`Profile model call failed: ${code}`);
5
+ this.code = code;
6
+ this.name = "ProfileModelError";
7
+ }
8
+ }
9
+ /** The code for a failed provider call result. */
10
+ export function profileResultCode(result) {
11
+ const message = result.error ?? "";
12
+ if (/timeout/i.test(message))
13
+ return "timeout";
14
+ if (result.httpStatus !== undefined)
15
+ return `http-${result.httpStatus}`;
16
+ if (/max iterations/i.test(message))
17
+ return "no-tool-call";
18
+ if (/validation/i.test(message))
19
+ return "invalid-reply";
20
+ return "error";
21
+ }
22
+ /** The code for any error thrown by a profile learning step. */
23
+ export function profileFailureCode(error) {
24
+ if (error instanceof ProfileModelError)
25
+ return error.code;
26
+ const message = error instanceof Error ? error.message : String(error);
27
+ if (/timeout/i.test(message))
28
+ return "timeout";
29
+ if (/invalid profile payload|validation/i.test(message))
30
+ return "invalid-reply";
31
+ if (/not configured|^Missing memory/i.test(message))
32
+ return "not-configured";
33
+ return "error";
34
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * A prompt too short to say anything about the user, such as "yes go". The
3
+ * word rule keeps short real preferences such as "use bun not npm".
4
+ */
5
+ export declare function isTrivialPrompt(text: string): boolean;