om-memory-system 5.2.0 → 5.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +4 -0
  2. package/dist/adapters/claude-code/hook-client.d.ts +1 -1
  3. package/dist/adapters/claude-code/hook-client.js +15 -2
  4. package/dist/adapters/opencode/in-process-client.d.ts +2 -0
  5. package/dist/adapters/opencode/in-process-client.js +16 -0
  6. package/dist/adapters/opencode/opencode-host-config.d.ts +3 -0
  7. package/dist/adapters/opencode/opencode-host-config.js +2 -0
  8. package/dist/adapters/opencode/tui-recall.d.ts +68 -0
  9. package/dist/adapters/opencode/tui-recall.js +312 -0
  10. package/dist/adapters/pi/extension.d.ts +1 -0
  11. package/dist/adapters/pi/extension.js +28 -11
  12. package/dist/adapters/pi/recall-rows.d.ts +31 -0
  13. package/dist/adapters/pi/recall-rows.js +148 -0
  14. package/dist/adapters/pi/session-context.d.ts +22 -0
  15. package/dist/adapters/pi/session-context.js +125 -0
  16. package/dist/config-template.d.ts +1 -1
  17. package/dist/config-template.js +20 -0
  18. package/dist/config.d.ts +8 -0
  19. package/dist/config.js +8 -4
  20. package/dist/core/context-budget.d.ts +2 -0
  21. package/dist/core/context-budget.js +2 -1
  22. package/dist/core/opening-context.d.ts +45 -0
  23. package/dist/core/opening-context.js +60 -0
  24. package/dist/core/recall-summary.d.ts +32 -0
  25. package/dist/core/recall-summary.js +77 -0
  26. package/dist/core/retrieval.d.ts +16 -0
  27. package/dist/core/retrieval.js +33 -11
  28. package/dist/importer/claude-hook-api.d.ts +5 -4
  29. package/dist/importer/claude-hook-api.js +58 -41
  30. package/dist/importer/claude-hook-context.d.ts +49 -0
  31. package/dist/importer/claude-hook-context.js +264 -0
  32. package/dist/index.js +1 -1
  33. package/dist/services/ai/local-api-url.d.ts +2 -0
  34. package/dist/services/ai/local-api-url.js +12 -0
  35. package/dist/services/ai/provider-config.d.ts +2 -0
  36. package/dist/services/ai/provider-config.js +3 -1
  37. package/dist/services/ai/providers/google-gemini.js +2 -1
  38. package/dist/services/ai/providers/openai-responses.js +2 -1
  39. package/dist/services/chat/chat-runtime.js +5 -1
  40. package/dist/services/chat/chat-status.js +4 -1
  41. package/dist/services/global-config-writer.js +39 -14
  42. package/dist/services/injection-options-config.d.ts +53 -0
  43. package/dist/services/injection-options-config.js +56 -0
  44. package/dist/services/settings-snapshot.js +44 -12
  45. package/dist/services/single-flight.d.ts +2 -0
  46. package/dist/services/single-flight.js +12 -0
  47. package/dist/services/visible-recall-config.d.ts +39 -0
  48. package/dist/services/visible-recall-config.js +28 -0
  49. package/dist/services/web-server.d.ts +3 -0
  50. package/dist/services/web-server.js +26 -8
  51. package/dist/utils/memory-limits.d.ts +2 -1
  52. package/dist/utils/memory-limits.js +6 -3
  53. package/dist/v2/adapter.d.ts +2 -0
  54. package/dist/v2/adapter.js +110 -31
  55. package/dist/v2/legacy-client.js +2 -1
  56. package/dist/v2/memory-bridge.d.ts +15 -0
  57. package/dist/v2/memory-bridge.js +21 -0
  58. package/dist/v2/recall-reporter.d.ts +27 -0
  59. package/dist/v2/recall-reporter.js +105 -0
  60. package/dist/v2/recall-rpc.d.ts +51 -0
  61. package/dist/v2/recall-rpc.js +38 -0
  62. package/dist/v2/session-parent.d.ts +14 -0
  63. package/dist/v2/session-parent.js +26 -0
  64. package/dist/web/assets/{index-CJ_K-hMG.js → index-BhQR88tU.js} +16 -16
  65. package/dist/web/assets/index-BmFUZbGr.css +2 -0
  66. package/dist/web/index.html +2 -2
  67. package/opencode/tui.tsx +22 -3
  68. package/package.json +1 -1
  69. package/dist/web/assets/index-ccH97VE2.css +0 -2
@@ -0,0 +1,148 @@
1
+ import { buildRecallSummary, callTitle, unfoldLines, } from "../../core/recall-summary.js";
2
+ import { log } from "../../services/logger.js";
3
+ /**
4
+ * Fold rows for OMMS recalls above the Pi editor (change add-pi-visible-recall, TDR-062).
5
+ *
6
+ * Rows are drawn only in Pi's interactive terminal (`ctx.mode === "tui"`). RPC, JSON and print
7
+ * runs, subagent children among them, record and draw nothing. Summaries and fold state live in
8
+ * memory only: Pi offers no extension store outside the session file, and a session entry would
9
+ * write memory text into the session file. Logs carry the session id, mode and count only.
10
+ */
11
+ export const RECALL_WIDGET_KEY = "omms-recall";
12
+ /** Latest summaries kept per session. */
13
+ export const MAX_RECALL_SUMMARIES = 20;
14
+ /** Most rows shown at once. */
15
+ export const MAX_RECALL_ROWS = 5;
16
+ /** Longest query kept per summary. An unfolded row shows at most 300 characters of it. */
17
+ export const MAX_RECALL_QUERY_CHARS = 300;
18
+ /** Cut by code point. Used only when Pi's own width helper cannot load. */
19
+ function plainCut(text, width) {
20
+ const chars = Array.from(text);
21
+ if (chars.length <= width)
22
+ return text;
23
+ if (width <= 3)
24
+ return chars.slice(0, Math.max(0, width)).join("");
25
+ return `${chars.slice(0, width - 3).join("")}...`;
26
+ }
27
+ // Pi resolves its own packages for an extension, so this import works inside Pi without a
28
+ // dependency (TDR-062). It stays dynamic so the extension carries no static Pi runtime import.
29
+ let cutLoad;
30
+ function loadCut() {
31
+ cutLoad ??= import("@earendil-works/pi-tui").then((tui) => (text, width) => tui.truncateToWidth(text, width), () => plainCut);
32
+ return cutLoad;
33
+ }
34
+ function paint(theme, text) {
35
+ try {
36
+ return theme.fg("muted", text);
37
+ }
38
+ catch {
39
+ return text;
40
+ }
41
+ }
42
+ function errorCode(error) {
43
+ return error instanceof Error ? error.name : "unknown";
44
+ }
45
+ export function createPiRecallRows() {
46
+ let sessionID = null;
47
+ let ring = [];
48
+ let lastSeq = 0;
49
+ const open = new Set();
50
+ const forget = () => {
51
+ sessionID = null;
52
+ ring = [];
53
+ lastSeq = 0;
54
+ open.clear();
55
+ };
56
+ const shown = () => ring.slice(-MAX_RECALL_ROWS);
57
+ const rowLines = () => shown().flatMap((summary) => {
58
+ const unfolded = open.has(summary.seq ?? 0);
59
+ const title = `${unfolded ? "▾" : "▸"} ● ${callTitle(summary)}`;
60
+ return unfolded ? [title, ...unfoldLines(summary).map((line) => ` ${line}`)] : [title];
61
+ });
62
+ const setWidget = (ctx, lines, cut) => {
63
+ try {
64
+ if (!lines) {
65
+ ctx.ui.setWidget(RECALL_WIDGET_KEY, undefined);
66
+ return;
67
+ }
68
+ ctx.ui.setWidget(RECALL_WIDGET_KEY, (_tui, theme) => ({
69
+ render(width) {
70
+ try {
71
+ return lines.map((line) => paint(theme, cut(line, width)));
72
+ }
73
+ catch {
74
+ return [];
75
+ }
76
+ },
77
+ invalidate() { },
78
+ }), { placement: "aboveEditor" });
79
+ }
80
+ catch (error) {
81
+ log("Pi recall: could not draw rows", { sessionID, code: errorCode(error) }, "error");
82
+ }
83
+ };
84
+ const draw = async (ctx, enabled) => {
85
+ const cut = await loadCut();
86
+ setWidget(ctx, enabled && ring.length > 0 ? rowLines() : undefined, cut);
87
+ };
88
+ return {
89
+ async record(ctx, query, section, enabled, mode = "search") {
90
+ try {
91
+ if (ctx.mode !== "tui" || !section)
92
+ return;
93
+ const built = buildRecallSummary(section, mode, query.slice(0, MAX_RECALL_QUERY_CHARS));
94
+ if (!built)
95
+ return;
96
+ const id = ctx.sessionManager.getSessionId();
97
+ if (id !== sessionID) {
98
+ forget();
99
+ sessionID = id;
100
+ }
101
+ const summary = { ...built, seq: ++lastSeq };
102
+ ring = [...ring, summary].slice(-MAX_RECALL_SUMMARIES);
103
+ for (const seq of open)
104
+ if (!ring.some((kept) => kept.seq === seq))
105
+ open.delete(seq);
106
+ log("Pi recall: summary recorded", {
107
+ sessionID: id,
108
+ mode: summary.mode,
109
+ count: summary.items.length,
110
+ });
111
+ await draw(ctx, enabled);
112
+ }
113
+ catch (error) {
114
+ log("Pi recall: could not record summary", { sessionID, code: errorCode(error) }, "error");
115
+ }
116
+ },
117
+ async toggleAll(ctx, enabled) {
118
+ try {
119
+ if (ctx.mode !== "tui")
120
+ return;
121
+ const seqs = shown().map((summary) => summary.seq ?? 0);
122
+ const unfold = seqs.some((seq) => !open.has(seq));
123
+ for (const seq of seqs) {
124
+ if (unfold)
125
+ open.add(seq);
126
+ else
127
+ open.delete(seq);
128
+ }
129
+ await draw(ctx, enabled);
130
+ }
131
+ catch (error) {
132
+ log("Pi recall: could not toggle rows", { sessionID, code: errorCode(error) }, "error");
133
+ }
134
+ },
135
+ reset(ctx) {
136
+ const hadRows = ring.length > 0;
137
+ forget();
138
+ try {
139
+ if (hadRows && ctx.mode === "tui")
140
+ ctx.ui.setWidget(RECALL_WIDGET_KEY, undefined);
141
+ }
142
+ catch (error) {
143
+ log("Pi recall: could not clear rows", { code: errorCode(error) }, "error");
144
+ }
145
+ },
146
+ summaries: () => ring,
147
+ };
148
+ }
@@ -0,0 +1,22 @@
1
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
2
+ import type { PiRecallRows } from "./recall-rows.js";
3
+ /**
4
+ * What Pi adds to the system prompt of one run beyond the prompt's own recall (change
5
+ * align-recall-host-parity, design D1d, D4, D5): the opening section on the first prompt of a
6
+ * main session, and the session's own memories after a compaction or a resume. In memory only.
7
+ */
8
+ /** Session ids remembered as seen, and restored sessions kept. The oldest is dropped first. */
9
+ export declare const MAX_TRACKED_SESSIONS = 500;
10
+ /** A Pi child started by om-pi-subagents (`--mode rpc --no-session --no-extensions`). */
11
+ export declare function isPiChild(): boolean;
12
+ export interface PiSessionContext {
13
+ /** Note that the session has had a prompt, so a later prompt is not its first. */
14
+ markSeen(sessionId: string): void;
15
+ /** Load the session's own memories for restore, replacing any earlier load. */
16
+ loadRestore(ctx: ExtensionContext): Promise<void>;
17
+ /** Text to append to the system prompt for this run, or null. */
18
+ recall(ctx: ExtensionContext, prompt: string, budgetBytes: number): Promise<string | null>;
19
+ /** Forget every restored session. */
20
+ reset(): void;
21
+ }
22
+ export declare function createPiSessionContext(rows: PiRecallRows): PiSessionContext;
@@ -0,0 +1,125 @@
1
+ import { CONFIG } from "../../config.js";
2
+ import { buildFirstPromptSections, openingQueryFor } from "../../core/opening-context.js";
3
+ import { buildRetrievalSection, formatMemoriesForCompaction, retrievalWrapperBytes, wrapRetrievalSection, } from "../../core/retrieval.js";
4
+ import { memoryClient } from "../../services/client.js";
5
+ import { log } from "../../services/logger.js";
6
+ import { getTags } from "../../services/tags.js";
7
+ import { utf8ByteLength } from "../../utils/context-limit.js";
8
+ /** The blank line before each section. The budget counts it for the second section on. */
9
+ const SECTION_SEPARATOR = "\n\n";
10
+ const SECTION_SEPARATOR_BYTES = 2;
11
+ /**
12
+ * What Pi adds to the system prompt of one run beyond the prompt's own recall (change
13
+ * align-recall-host-parity, design D1d, D4, D5): the opening section on the first prompt of a
14
+ * main session, and the session's own memories after a compaction or a resume. In memory only.
15
+ */
16
+ /** Session ids remembered as seen, and restored sessions kept. The oldest is dropped first. */
17
+ export const MAX_TRACKED_SESSIONS = 500;
18
+ const HOST = "pi";
19
+ /** A Pi child started by om-pi-subagents (`--mode rpc --no-session --no-extensions`). */
20
+ export function isPiChild() {
21
+ return process.env.OMPS_CHILD === "1";
22
+ }
23
+ function hasEarlierUserEntry(branch) {
24
+ return (Array.isArray(branch) &&
25
+ branch.some((entry) => entry?.type === "message" && entry.message?.role === "user"));
26
+ }
27
+ function remember(map, key, value) {
28
+ map.delete(key);
29
+ map.set(key, value);
30
+ if (map.size > MAX_TRACKED_SESSIONS)
31
+ map.delete(map.keys().next().value);
32
+ }
33
+ export function createPiSessionContext(rows) {
34
+ const seen = new Map();
35
+ const restored = new Map();
36
+ const restoreEnabled = () => !isPiChild() && CONFIG.compaction?.enabled === true;
37
+ async function loadRestore(ctx) {
38
+ if (!restoreEnabled())
39
+ return;
40
+ const sessionId = ctx.sessionManager.getSessionId();
41
+ try {
42
+ const result = await memoryClient.searchMemoriesBySessionID(sessionId, getTags(ctx.cwd).project.tag, CONFIG.compaction.memoryLimit);
43
+ const own = result.success
44
+ ? result.results.filter((memory) => memory.metadata?.host === HOST)
45
+ : [];
46
+ if (own.length > 0)
47
+ remember(restored, sessionId, { memories: own, announced: false });
48
+ else
49
+ restored.delete(sessionId);
50
+ }
51
+ catch (error) {
52
+ log("Pi restore load failed", { code: error instanceof Error ? error.name : "unknown" }, "error");
53
+ }
54
+ }
55
+ return {
56
+ markSeen(sessionId) {
57
+ remember(seen, sessionId, true);
58
+ },
59
+ loadRestore,
60
+ async recall(ctx, prompt, budgetBytes) {
61
+ const sessionId = ctx.sessionManager.getSessionId();
62
+ const child = isPiChild();
63
+ const visible = CONFIG.visibleRecall !== false;
64
+ const budget = { maxBytes: budgetBytes, wrapperBytes: retrievalWrapperBytes() };
65
+ // A session this process has not seen: first prompt when its branch holds no user entry,
66
+ // else a resumed session.
67
+ const unseen = !child && !seen.has(sessionId);
68
+ if (unseen)
69
+ remember(seen, sessionId, true);
70
+ const resumed = unseen && hasEarlierUserEntry(ctx.sessionManager.getBranch());
71
+ const openingQuery = unseen && !resumed
72
+ ? openingQueryFor({
73
+ openingContext: CONFIG.openingContext,
74
+ openingContextQuery: CONFIG.openingContextQuery,
75
+ })
76
+ : null;
77
+ let promptSection = null;
78
+ let openingSection = null;
79
+ if (CONFIG.chatMessage.enabled && (!child || CONFIG.subagentMemory)) {
80
+ if (openingQuery) {
81
+ ({ promptSection, openingSection } = await buildFirstPromptSections({
82
+ prompt,
83
+ openingQuery,
84
+ directory: ctx.cwd,
85
+ sessionId,
86
+ budget,
87
+ openingReserveBytes: SECTION_SEPARATOR_BYTES,
88
+ }));
89
+ }
90
+ else {
91
+ const section = await buildRetrievalSection(prompt, ctx.cwd, sessionId, budget);
92
+ promptSection = section ? wrapRetrievalSection(section) : null;
93
+ }
94
+ }
95
+ if (resumed)
96
+ await loadRestore(ctx);
97
+ let restoredSection = null;
98
+ const entry = restoreEnabled() ? restored.get(sessionId) : undefined;
99
+ if (entry) {
100
+ const left = budgetBytes -
101
+ (promptSection ? utf8ByteLength(promptSection) + SECTION_SEPARATOR_BYTES : 0);
102
+ const text = left > budget.wrapperBytes
103
+ ? formatMemoriesForCompaction(entry.memories, {
104
+ maxBytes: left,
105
+ wrapperBytes: budget.wrapperBytes,
106
+ })
107
+ : "";
108
+ restoredSection = text ? wrapRetrievalSection(text) : null;
109
+ }
110
+ // Screen-only rows; the returned text stays the same with them on or off.
111
+ await rows.record(ctx, prompt, promptSection, visible);
112
+ if (openingSection)
113
+ await rows.record(ctx, openingQuery ?? "", openingSection, visible, "opening");
114
+ if (restoredSection && entry && !entry.announced) {
115
+ entry.announced = true;
116
+ await rows.record(ctx, "", restoredSection, visible, "restore");
117
+ }
118
+ const parts = [promptSection, openingSection, restoredSection].filter(Boolean);
119
+ return parts.length > 0 ? SECTION_SEPARATOR + parts.join(SECTION_SEPARATOR) : null;
120
+ },
121
+ reset() {
122
+ restored.clear();
123
+ },
124
+ };
125
+ }
@@ -3,4 +3,4 @@
3
3
  * src/config.ts until that file reached its size ceiling; the text is
4
4
  * unchanged. Shared by config.ts and the global settings writer.
5
5
  */
6
- export declare const CONFIG_TEMPLATE = "{\n // ============================================\n // omms (Opinionated Modular Memory System) Configuration\n // ============================================\n \n // Storage location for the vector database. Leave unset to use the\n // default ~/.omms/data (a legacy ~/.opencode-mem/data store is migrated\n // there automatically on first start; see docs/omms-migration.md).\n // \"storagePath\": \"~/.omms/data\",\n\n \"userEmailOverride\": \"\",\n \"userNameOverride\": \"\",\n \n // ============================================\n // Embedding Model (for similarity search)\n // ============================================\n // Local = Hugging Face / ONNX via @huggingface/transformers (not Apple MLX).\n // Remote = set embeddingApiUrl (OpenAI-compatible /embeddings); embeddingApiKey is optional.\n \n // Default: Nomic Embed v1 (768 dimensions, 8192 context, multilingual)\n \"embeddingModel\": \"Xenova/nomic-embed-text-v1\",\n\n // Opt-in Nomic task prefixes (search_document: / search_query:). After enabling,\n // re-index existing memories so store and query vectors stay aligned.\n // \"embeddingUseTaskPrefixes\": true,\n \n // Auto-detected dimensions (no need to set manually)\n // \"embeddingDimensions\": 768,\n \n // Other recommended local models:\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-base-en\", // 768 dims, English-only, 8192 context\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-small-en\", // 512 dims, faster, 8192 context\n // \"embeddingModel\": \"Xenova/all-MiniLM-L6-v2\", // 384 dims, very fast, 512 context\n // \"embeddingModel\": \"Xenova/all-mpnet-base-v2\", // 768 dims, good quality, 512 context\n \n // Optional: OpenAI-compatible API for embeddings (URL required; key only if the server needs one)\n // \"embeddingApiUrl\": \"https://api.openai.com/v1\",\n // \"embeddingApiKey\": \"env://OPENAI_API_KEY\", // or \"sk-...\" / \"file:///path/to/key\"\n // \"embeddingModel\": \"text-embedding-3-small\", // 1536 dims, auto-detected\n \n // ============================================\n // Web Server Settings\n // ============================================\n \n // Start a background import of past chats on each host's next start.\n \"autoBackfill\": true,\n // \"piBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // \"opencodeBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // Directory maps for history recorded in moved or deleted directories.\n // \"importPathMaps\": [{ \"from\": \"~/code/app-feat-x\", \"to\": \"~/code/app\" }],\n // Unresolved directories to hide on the Directory maps page. They stay unimported.\n // \"importIgnoredDirectories\": [\"/private/tmp/scratch\"],\n // Claude Code's folder, when it is not ~/.claude (like CLAUDE_CONFIG_DIR).\n // \"claudeConfigDir\": \"~/.claude-work\",\n\n // Register the web app to start when you log in.\n \"webServerAutoStart\": true,\n // Enable web UI for managing memories (accessible at http://localhost:4747)\n \"webServerEnabled\": true,\n \n // Port for web UI server\n \"webServerPort\": 4747,\n \n // Host address for web UI (use 127.0.0.1 for local only, 0.0.0.0 for network access)\n \"webServerHost\": \"127.0.0.1\",\n\n // Optional HTTP Basic Auth for the web UI. Accepts literal, env://, or file:// secrets.\n // \"webServerAuthPassword\": \"\",\n // \"webServerAuthUsername\": \"\",\n\n // Deprecated: imported once into the API token table, then no longer read.\n // Create API tokens on the Settings page (Keys and access) instead.\n // \"webServerApiToken\": \"env://OMMS_WEB_TOKEN\",\n \n // ============================================\n // Database Settings\n // ============================================\n \n // Maximum vectors per database shard (auto-creates new shard when limit reached)\n \"maxVectorsPerShard\": 50000,\n \n // Automatically delete old memories based on retention period\n \"autoCleanupEnabled\": true,\n \n // Days to keep memories before auto-cleanup (only if autoCleanupEnabled is true)\n \"autoCleanupRetentionDays\": 30,\n \n // Automatically detect and remove duplicate memories\n \"deduplicationEnabled\": true,\n \n // Similarity threshold (0-1) for detecting duplicates (higher = stricter)\n \"deduplicationSimilarityThreshold\": 0.90,\n \n // ============================================\n // Memory Scope Settings\n // ============================================\n\n // Default scope for memory list/search queries\n // \"project\" keeps queries within the current project, \"all-projects\" searches across all project shards\n \"memory\": {\n \"defaultScope\": \"project\"\n },\n\n // ============================================\n // Model for auto-capture and profile learning (same rule in OpenCode and Pi)\n // ============================================\n\n // Which model summarises your work, in this order:\n // 1. The host model below: opencodeProvider/opencodeModel in OpenCode,\n // piProvider/piModel in Pi. Set the model to \"inherit\" to follow\n // whatever model the session is using, or to \"external\" to send\n // every call to the external API below.\n // 2. If no host model is set: the external API (memoryModel/memoryApiUrl/\n // memoryApiKey further down).\n // 3. If neither is set: the session's own model.\n // If the host model fails and the external API is configured, the external\n // API is used instead.\n //\n // Host models go through OpenCode's or Pi's own sign-in (OAuth like Claude\n // Pro/Max, GitHub Copilot, ChatGPT, or your own key there), so no separate API\n // key is needed here.\n //\n // Examples (OpenCode names from 'opencode providers list'; Pi names from its model list):\n // \"opencodeProvider\": \"anthropic\", \"opencodeModel\": \"claude-haiku-4-5-20251001\"\n // \"opencodeProvider\": \"openai\", \"opencodeModel\": \"inherit\"\n // \"piProvider\": \"openai-codex\", \"piModel\": \"gpt-5.6-luna\"\n //\n // \"opencodeProvider\": \"anthropic\",\n // \"opencodeModel\": \"claude-haiku-4-5-20251001\",\n // \"piProvider\": \"openai-codex\",\n // \"piModel\": \"gpt-5.6-luna\",\n\n // ============================================\n // Auto-Capture Settings\n // ============================================\n \n // Auto-capture runs in the background without blocking your main session.\n // Note: Ollama may not support tool calling. Use OpenAI, Anthropic, or Groq for best results.\n \n \"autoCaptureEnabled\": true,\n \n // Provider type: \"openai-chat\" | \"openai-responses\" | \"anthropic\" | \"minimax\" | \"orcarouter\" | \"google-gemini\"\n // Note: \"openai-chat\" is a generic OpenAI API-compatible mode.\n // Any service that follows the OpenAI Chat Completions API can use it via custom \"memoryApiUrl\".\n \"memoryProvider\": \"openai-chat\",\n \n // External API (step 2 above, and the fallback when a host model fails).\n // Uncomment all 3 lines and replace memoryApiKey before use:\n // \"memoryModel\": \"gpt-4o-mini\",\n // \"memoryApiUrl\": \"https://api.openai.com/v1\",\n // \"memoryApiKey\": \"sk-...\",\n\n // API Key Formats:\n // Direct value: \"sk-...\"\n // From file: \"file://~/.config/litellm-key.txt\"\n // From env variable: \"env://LITELLM_API_KEY\"\n \n // Examples for different providers:\n // Any OpenAI-compatible endpoint can use the \"openai-chat\" provider pattern below.\n // Common examples: DeepSeek, Qwen (via Alibaba Cloud ModelStudio),\n // Zhipu GLM (BigModel platform), and Kimi (Moonshot AI platform).\n\n // OpenAI Chat Completion (default, backward compatible):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"gpt-4o-mini\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n\n // DeepSeek (OpenAI-compatible example):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"deepseek-chat\"\n // \"memoryApiUrl\": \"https://api.deepseek.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // OpenAI Responses API (recommended, with session support):\n // \"memoryProvider\": \"openai-responses\"\n // \"memoryModel\": \"gpt-4o\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // Anthropic (with session support):\n // \"memoryProvider\": \"anthropic\"\n // \"memoryModel\": \"claude-3-5-haiku-20241022\"\n // \"memoryApiUrl\": \"https://api.anthropic.com/v1\"\n // \"memoryApiKey\": \"sk-ant-...\"\n\n // MiniMax (Anthropic Messages-compatible endpoint, with session support):\n // \"memoryProvider\": \"minimax\"\n // \"memoryModel\": \"MiniMax-M3\"\n // \"memoryApiUrl\": \"https://api.minimax.io\" // global endpoint\n // \"memoryApiKey\": \"<MiniMax API key>\"\n // // China endpoint: \"memoryApiUrl\": \"https://api.minimaxi.com\"\n // // Optional adaptive thinking for MiniMax-M3:\n // \"memoryExtraParams\": { \"thinking\": { \"type\": \"adaptive\" } }\n\n // OrcaRouter (OpenAI-compatible gateway, namespaced model IDs, with session support):\n // \"memoryProvider\": \"orcarouter\"\n // \"memoryApiKey\": \"<OrcaRouter API key>\"\n // // memoryApiUrl and memoryModel are optional \u2014 they default to\n // // https://api.orcarouter.ai/v1 and \"orcarouter/auto\" (a routing alias).\n // // OrcaRouter rejects bare model names, so if you set memoryModel, use a\n // // namespaced ID such as \"openai/gpt-5.5\" or \"deepseek/deepseek-v4-flash\".\n // \"memoryModel\": \"openai/gpt-5.5\"\n\n // Groq (OpenAI-compatible, use openai-chat provider):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"llama-3.3-70b-versatile\"\n // \"memoryApiUrl\": \"https://api.groq.com/openai/v1\"\n // \"memoryApiKey\": \"gsk_...\"\n \n // Maximum iterations for multi-turn AI analysis (for openai-responses, anthropic, and minimax)\n \"autoCaptureMaxIterations\": 5,\n \n // Timeout per iteration in milliseconds (30 seconds default)\n \"autoCaptureIterationTimeout\": 30000,\n\n // Tries for one turn's capture, on Pi and OpenCode, 2 and 4 seconds apart. After the\n // last one fails, a turn the model could not reach goes to the capture retry queue.\n \"autoCaptureMaxRetries\": 3,\n\n // Maximum UTF-8 bytes for the auto-capture markdown context sent to the summary model.\n // Prevents HTTP 400 context overflows on models with ~131K token windows (e.g. Groq Llama).\n // Rough guide: tokens \u2248 bytes / 4 for mixed code/prose.\n \"autoCaptureMaxContextBytes\": 131072,\n \n // Days to keep AI session history before cleanup\n \"aiSessionRetentionDays\": 7,\n\n // Capture diagnostics: every capture attempt always writes one metadata line\n // (model, stop reason, sizes, outcome) to ~/.omms/omms.log, with no\n // conversation text. Set captureTrace to true to also write each attempt's\n // prompt and raw reply to ~/.omms/traces/ for debugging. Traces can contain\n // conversation content: <private> text and common API key formats are\n // redacted, files are readable only by you, and they are deleted after\n // captureTraceRetentionDays. Only this global file can turn tracing on.\n // \"captureTrace\": false,\n // \"captureTraceRetentionDays\": 7,\n // \"captureAttemptRetentionDays\": 30,\n\n // Skill learning: record each full turn (prompt, replies, tool calls with\n // their outcome, and the skills used) in ~/.omms/data/skills.db, so OMMS can\n // rank your skills and draft better versions. Off by default. A project config\n // can turn it off but never on. No model call is made to record a turn.\n // \"skillLearning\": false,\n // Remove <private> text and common API key formats before a turn is stored.\n // Global config only. Turning this off stores text as written.\n // \"skillLearningRedact\": true,\n // Folders OMMS reads skills from. Absolute or starting with ~/. Global config\n // only. Unset: ~/.agents/skills and the Claude Code folder's skills folder.\n // \"skillFolders\": [\"~/.agents/skills\"],\n\n // Capture retry queue: when a live capture fails because the model cannot be\n // reached (network error, timeout, HTTP 408, 429 or 5xx), OMMS keeps a copy of\n // the turn in ~/.omms/data and tries it again later. The copy has <private>\n // text and common API key formats removed. Turns are deleted when the retry\n // ends or after captureRetryRetentionHours (0 to 720). 0 turns the queue off.\n // Only this global file can set it.\n // \"captureRetryRetentionHours\": 72,\n\n // Temperature for AI API requests (set to false to omit parameter for models that don't support it)\n // Some reasoning models (like o1, o3, gpt-5) don't support temperature parameter\n // Set to false and add \"memoryTemperature\": false in config when using such models\n \"memoryTemperature\": 0.3,\n\n // Extra parameters to include in API request body\n // Useful for local inference servers (e.g. llama-server with --jinja) that support\n // additional parameters like disabling thinking/reasoning mode\n // Example for Qwen3 models: { \"enable_thinking\": false }\n // \"memoryExtraParams\": {},\n\n // Language for auto-capture summaries (default: \"auto\" for auto-detection)\n // Options: \"auto\", \"en\", \"id\", \"zh\", \"ja\", \"es\", \"fr\", \"de\", \"ru\", \"pt\", \"ar\", \"ko\"\n // \"autoCaptureLanguage\": \"auto\",\n\n // ============================================\n // Toast Notifications\n // ============================================\n\n // Show toast when memory is auto-captured\n \"showAutoCaptureToasts\": true,\n\n // Show toast when user profile is updated\n \"showUserProfileToasts\": true,\n\n // Show toast for error messages\n \"showErrorToasts\": true,\n\n // ============================================\n // User Profile System\n // ============================================\n\n // Analyze user prompts every N prompts to build/update your user profile\n // When N uncaptured prompts accumulate, AI will analyze them to identify:\n // - User preferences (code style, communication style, tool preferences)\n // - User patterns (recurring topics, problem domains, technical interests)\n // - User workflows (development habits, sequences, learning style)\n // - Skill level (overall and per-domain assessment)\n \"userProfileAnalysisInterval\": 10,\n\n // Days before inactive items (all types) are eligible for removal\n \"userProfileStaleDays\": 2,\n\n // Days before an inactive workflow is eligible for removal. A workflow seen\n // userProfileMinEvidenceForRetention times or more is kept.\n \"userProfileWorkflowStaleDays\": 30,\n\n // Number of preferences shown in UI\n \"userProfileDisplayPreferences\": 20,\n \n // Number of patterns shown in UI\n \"userProfileDisplayPatterns\": 15,\n \n // Number of workflows shown in UI\n \"userProfileDisplayWorkflows\": 10,\n \n // Number of preferences injected into LLM conversation context\n // Keep this small \u2014 the strongest signals are enough; more dilute LLM attention\n \"userProfileInjectPreferences\": 5,\n \n // Number of patterns injected into LLM conversation context\n \"userProfileInjectPatterns\": 5,\n \n // Number of workflows injected into LLM conversation context\n \"userProfileInjectWorkflows\": 3,\n \n // Days before preference confidence starts to decay (if not reinforced)\n // Preferences that aren't seen again will gradually lose confidence and be removed\n \"userProfileConfidenceDecayDays\": 30,\n \n // Number of profile versions to keep in changelog (for rollback/debugging)\n // Older versions are automatically cleaned up\n \"userProfileChangelogRetentionCount\": 5,\n\n // Minimum evidence count for a preference/pattern to survive confidence decay\n // Items confirmed fewer times are more likely to be pruned when confidence decays\n \"userProfileMinEvidenceForRetention\": 3,\n\n // Merge duplicate or vague profile items every N analysed prompts (about 1 model call per 100 items)\n \"userProfileAutoCleanupEnabled\": true,\n \"userProfileAutoCleanupInterval\": 500,\n\n // Enable LLM validation of existing preferences against recent behavior.\n // When enabled, each analysis round checks if top-5 preferences still match recent prompts.\n // Experimental \u2014 disabled by default.\n \"userProfileValidationEnabled\": false,\n\n // ============================================\n // Search Settings\n // ============================================\n \n // Minimum similarity score (0-1) for memory search results. Leave unset to\n // follow the default for your embedding model: 0.5 for qwen3-embedding:0.6b\n // (the only model measured), 0.6 for every other embedder.\n // \"similarityThreshold\": 0.5,\n\n // Maximum number of memories to return in search results.\n // Manual searches can ask for fewer; prompt retrieval uses this ceiling.\n \"maxMemories\": 10,\n\n // ============================================\n // Memory Context Controls\n // ============================================\n\n // Recent memories added at fresh session start (OpenCode V1 and Claude Code).\n // Pi and OpenCode V2 search the store with each prompt instead.\n \"chatMessage\": {\n \"enabled\": true,\n // Recent memories injected at session start.\n \"maxMemories\": 3,\n \"excludeCurrentSession\": true,\n // When to inject: \"first\" message of a session or \"always\".\n // \"injectOn\": \"first\",\n },\n\n // OpenCode profile-learning input limit, in UTF-8 bytes. Smaller values can\n // omit prompts from that input. Does not limit Pi or Claude Code profile input.\n \"userProfileMaxContextBytes\": 32768,\n\n // Shared budget for automatic memory context (profile text, memories, and\n // formatting) across all hosts, in approximate tokens: ceil(UTF-8 bytes / 4).\n // Covers prompt retrieval, recent-memory injection, and session-memory\n // restoration. Not an exact provider token limit or a spending limit.\n \"retrievalMaxTokens\": 2000,\n\n // ============================================\n // Advanced Settings\n // ============================================\n \n // Inject user profile into AI context (preferences, patterns, workflows)\n \"injectProfile\": true\n}\n";
6
+ export declare const CONFIG_TEMPLATE = "{\n // ============================================\n // omms (Opinionated Modular Memory System) Configuration\n // ============================================\n \n // Storage location for the vector database. Leave unset to use the\n // default ~/.omms/data (a legacy ~/.opencode-mem/data store is migrated\n // there automatically on first start; see docs/omms-migration.md).\n // \"storagePath\": \"~/.omms/data\",\n\n \"userEmailOverride\": \"\",\n \"userNameOverride\": \"\",\n \n // ============================================\n // Embedding Model (for similarity search)\n // ============================================\n // Local = Hugging Face / ONNX via @huggingface/transformers (not Apple MLX).\n // Remote = set embeddingApiUrl (OpenAI-compatible /embeddings); embeddingApiKey is optional.\n \n // Default: Nomic Embed v1 (768 dimensions, 8192 context, multilingual)\n \"embeddingModel\": \"Xenova/nomic-embed-text-v1\",\n\n // Opt-in Nomic task prefixes (search_document: / search_query:). After enabling,\n // re-index existing memories so store and query vectors stay aligned.\n // \"embeddingUseTaskPrefixes\": true,\n \n // Auto-detected dimensions (no need to set manually)\n // \"embeddingDimensions\": 768,\n \n // Other recommended local models:\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-base-en\", // 768 dims, English-only, 8192 context\n // \"embeddingModel\": \"Xenova/jina-embeddings-v2-small-en\", // 512 dims, faster, 8192 context\n // \"embeddingModel\": \"Xenova/all-MiniLM-L6-v2\", // 384 dims, very fast, 512 context\n // \"embeddingModel\": \"Xenova/all-mpnet-base-v2\", // 768 dims, good quality, 512 context\n \n // Optional: OpenAI-compatible API for embeddings (URL required; key only if the server needs one)\n // \"embeddingApiUrl\": \"https://api.openai.com/v1\",\n // \"embeddingApiKey\": \"env://OPENAI_API_KEY\", // or \"sk-...\" / \"file:///path/to/key\"\n // \"embeddingModel\": \"text-embedding-3-small\", // 1536 dims, auto-detected\n \n // ============================================\n // Web Server Settings\n // ============================================\n \n // Start a background import of past chats on each host's next start.\n \"autoBackfill\": true,\n // \"piBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // \"opencodeBackfillModel\": \"inherit\", // or \"external\", or \"provider/model\"\n // Directory maps for history recorded in moved or deleted directories.\n // \"importPathMaps\": [{ \"from\": \"~/code/app-feat-x\", \"to\": \"~/code/app\" }],\n // Unresolved directories to hide on the Directory maps page. They stay unimported.\n // \"importIgnoredDirectories\": [\"/private/tmp/scratch\"],\n // Claude Code's folder, when it is not ~/.claude (like CLAUDE_CONFIG_DIR).\n // \"claudeConfigDir\": \"~/.claude-work\",\n\n // Register the web app to start when you log in.\n \"webServerAutoStart\": true,\n // Enable web UI for managing memories (accessible at http://localhost:4747)\n \"webServerEnabled\": true,\n \n // Port for web UI server\n \"webServerPort\": 4747,\n \n // Host address for web UI (use 127.0.0.1 for local only, 0.0.0.0 for network access)\n \"webServerHost\": \"127.0.0.1\",\n\n // Optional HTTP Basic Auth for the web UI. Accepts literal, env://, or file:// secrets.\n // \"webServerAuthPassword\": \"\",\n // \"webServerAuthUsername\": \"\",\n\n // Deprecated: imported once into the API token table, then no longer read.\n // Create API tokens on the Settings page (Keys and access) instead.\n // \"webServerApiToken\": \"env://OMMS_WEB_TOKEN\",\n \n // ============================================\n // Database Settings\n // ============================================\n \n // Maximum vectors per database shard (auto-creates new shard when limit reached)\n \"maxVectorsPerShard\": 50000,\n \n // Automatically delete old memories based on retention period\n \"autoCleanupEnabled\": true,\n \n // Days to keep memories before auto-cleanup (only if autoCleanupEnabled is true)\n \"autoCleanupRetentionDays\": 30,\n \n // Automatically detect and remove duplicate memories\n \"deduplicationEnabled\": true,\n \n // Similarity threshold (0-1) for detecting duplicates (higher = stricter)\n \"deduplicationSimilarityThreshold\": 0.90,\n \n // ============================================\n // Memory Scope Settings\n // ============================================\n\n // Default scope for memory list/search queries\n // \"project\" keeps queries within the current project, \"all-projects\" searches across all project shards\n \"memory\": {\n \"defaultScope\": \"project\"\n },\n\n // ============================================\n // Model for auto-capture and profile learning (same rule in OpenCode and Pi)\n // ============================================\n\n // Which model summarises your work, in this order:\n // 1. The host model below: opencodeProvider/opencodeModel in OpenCode,\n // piProvider/piModel in Pi. Set the model to \"inherit\" to follow\n // whatever model the session is using, or to \"external\" to send\n // every call to the external API below.\n // 2. If no host model is set: the external API (memoryModel/memoryApiUrl/\n // memoryApiKey further down).\n // 3. If neither is set: the session's own model.\n // If the host model fails and the external API is configured, the external\n // API is used instead.\n //\n // Host models go through OpenCode's or Pi's own sign-in (OAuth like Claude\n // Pro/Max, GitHub Copilot, ChatGPT, or your own key there), so no separate API\n // key is needed here.\n //\n // Examples (OpenCode names from 'opencode providers list'; Pi names from its model list):\n // \"opencodeProvider\": \"anthropic\", \"opencodeModel\": \"claude-haiku-4-5-20251001\"\n // \"opencodeProvider\": \"openai\", \"opencodeModel\": \"inherit\"\n // \"piProvider\": \"openai-codex\", \"piModel\": \"gpt-5.6-luna\"\n //\n // \"opencodeProvider\": \"anthropic\",\n // \"opencodeModel\": \"claude-haiku-4-5-20251001\",\n // \"piProvider\": \"openai-codex\",\n // \"piModel\": \"gpt-5.6-luna\",\n\n // ============================================\n // Auto-Capture Settings\n // ============================================\n \n // Auto-capture runs in the background without blocking your main session.\n // Note: Ollama may not support tool calling. Use OpenAI, Anthropic, or Groq for best results.\n \n \"autoCaptureEnabled\": true,\n \n // Provider type: \"openai-chat\" | \"openai-responses\" | \"anthropic\" | \"minimax\" | \"orcarouter\" | \"google-gemini\"\n // Note: \"openai-chat\" is a generic OpenAI API-compatible mode.\n // Any service that follows the OpenAI Chat Completions API can use it via custom \"memoryApiUrl\".\n \"memoryProvider\": \"openai-chat\",\n \n // External API (step 2 above, and the fallback when a host model fails).\n // Uncomment all 3 lines and replace memoryApiKey before use:\n // \"memoryModel\": \"gpt-4o-mini\",\n // \"memoryApiUrl\": \"https://api.openai.com/v1\",\n // \"memoryApiKey\": \"sk-...\",\n\n // API Key Formats:\n // Direct value: \"sk-...\"\n // From file: \"file://~/.config/litellm-key.txt\"\n // From env variable: \"env://LITELLM_API_KEY\"\n \n // Examples for different providers:\n // Any OpenAI-compatible endpoint can use the \"openai-chat\" provider pattern below.\n // Common examples: DeepSeek, Qwen (via Alibaba Cloud ModelStudio),\n // Zhipu GLM (BigModel platform), and Kimi (Moonshot AI platform).\n\n // OpenAI Chat Completion (default, backward compatible):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"gpt-4o-mini\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n\n // DeepSeek (OpenAI-compatible example):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"deepseek-chat\"\n // \"memoryApiUrl\": \"https://api.deepseek.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // OpenAI Responses API (recommended, with session support):\n // \"memoryProvider\": \"openai-responses\"\n // \"memoryModel\": \"gpt-4o\"\n // \"memoryApiUrl\": \"https://api.openai.com/v1\"\n // \"memoryApiKey\": \"sk-...\"\n \n // Anthropic (with session support):\n // \"memoryProvider\": \"anthropic\"\n // \"memoryModel\": \"claude-3-5-haiku-20241022\"\n // \"memoryApiUrl\": \"https://api.anthropic.com/v1\"\n // \"memoryApiKey\": \"sk-ant-...\"\n\n // MiniMax (Anthropic Messages-compatible endpoint, with session support):\n // \"memoryProvider\": \"minimax\"\n // \"memoryModel\": \"MiniMax-M3\"\n // \"memoryApiUrl\": \"https://api.minimax.io\" // global endpoint\n // \"memoryApiKey\": \"<MiniMax API key>\"\n // // China endpoint: \"memoryApiUrl\": \"https://api.minimaxi.com\"\n // // Optional adaptive thinking for MiniMax-M3:\n // \"memoryExtraParams\": { \"thinking\": { \"type\": \"adaptive\" } }\n\n // OrcaRouter (OpenAI-compatible gateway, namespaced model IDs, with session support):\n // \"memoryProvider\": \"orcarouter\"\n // \"memoryApiKey\": \"<OrcaRouter API key>\"\n // // memoryApiUrl and memoryModel are optional \u2014 they default to\n // // https://api.orcarouter.ai/v1 and \"orcarouter/auto\" (a routing alias).\n // // OrcaRouter rejects bare model names, so if you set memoryModel, use a\n // // namespaced ID such as \"openai/gpt-5.5\" or \"deepseek/deepseek-v4-flash\".\n // \"memoryModel\": \"openai/gpt-5.5\"\n\n // Groq (OpenAI-compatible, use openai-chat provider):\n // \"memoryProvider\": \"openai-chat\"\n // \"memoryModel\": \"llama-3.3-70b-versatile\"\n // \"memoryApiUrl\": \"https://api.groq.com/openai/v1\"\n // \"memoryApiKey\": \"gsk_...\"\n \n // Maximum iterations for multi-turn AI analysis (for openai-responses, anthropic, and minimax)\n \"autoCaptureMaxIterations\": 5,\n \n // Timeout per iteration in milliseconds (30 seconds default)\n \"autoCaptureIterationTimeout\": 30000,\n\n // Tries for one turn's capture, on Pi and OpenCode, 2 and 4 seconds apart. After the\n // last one fails, a turn the model could not reach goes to the capture retry queue.\n \"autoCaptureMaxRetries\": 3,\n\n // Maximum UTF-8 bytes for the auto-capture markdown context sent to the summary model.\n // Prevents HTTP 400 context overflows on models with ~131K token windows (e.g. Groq Llama).\n // Rough guide: tokens \u2248 bytes / 4 for mixed code/prose.\n \"autoCaptureMaxContextBytes\": 131072,\n \n // Days to keep AI session history before cleanup\n \"aiSessionRetentionDays\": 7,\n\n // Capture diagnostics: every capture attempt always writes one metadata line\n // (model, stop reason, sizes, outcome) to ~/.omms/omms.log, with no\n // conversation text. Set captureTrace to true to also write each attempt's\n // prompt and raw reply to ~/.omms/traces/ for debugging. Traces can contain\n // conversation content: <private> text and common API key formats are\n // redacted, files are readable only by you, and they are deleted after\n // captureTraceRetentionDays. Only this global file can turn tracing on.\n // \"captureTrace\": false,\n // \"captureTraceRetentionDays\": 7,\n // \"captureAttemptRetentionDays\": 30,\n\n // Skill learning: record each full turn (prompt, replies, tool calls with\n // their outcome, and the skills used) in ~/.omms/data/skills.db, so OMMS can\n // rank your skills and draft better versions. Off by default. A project config\n // can turn it off but never on. No model call is made to record a turn.\n // \"skillLearning\": false,\n // Remove <private> text and common API key formats before a turn is stored.\n // Global config only. Turning this off stores text as written.\n // \"skillLearningRedact\": true,\n // Folders OMMS reads skills from. Absolute or starting with ~/. Global config\n // only. Unset: ~/.agents/skills and the Claude Code folder's skills folder.\n // \"skillFolders\": [\"~/.agents/skills\"],\n\n // Visible recall: the OpenCode terminal TUI and Pi's interactive terminal draw\n // a row for each memory recall. On by default. A project config can turn it\n // off but never on.\n // \"visibleRecall\": true,\n\n // Memory injection, on OpenCode V2, Pi and Claude Code. Each prompt gets the\n // memories that match it. openingContext also searches memory with\n // openingContextQuery and adds the matches to the\n // first message of a new session. The query is global only, trimmed, and\n // 500 characters at most. subagentMemory gives each subagent the memories\n // that match its task. For both switches, a project config can only turn these off.\n // \"openingContext\": false,\n // \"openingContextQuery\": \"project architecture, setup, conventions and key decisions\",\n // \"subagentMemory\": true,\n\n // Capture retry queue: when a live capture fails because the model cannot be\n // reached (network error, timeout, HTTP 408, 429 or 5xx), OMMS keeps a copy of\n // the turn in ~/.omms/data and tries it again later. The copy has <private>\n // text and common API key formats removed. Turns are deleted when the retry\n // ends or after captureRetryRetentionHours (0 to 720). 0 turns the queue off.\n // Only this global file can set it.\n // \"captureRetryRetentionHours\": 72,\n\n // Temperature for AI API requests (set to false to omit parameter for models that don't support it)\n // Some reasoning models (like o1, o3, gpt-5) don't support temperature parameter\n // Set to false and add \"memoryTemperature\": false in config when using such models\n \"memoryTemperature\": 0.3,\n\n // Extra parameters to include in API request body\n // Useful for local inference servers (e.g. llama-server with --jinja) that support\n // additional parameters like disabling thinking/reasoning mode\n // Example for Qwen3 models: { \"enable_thinking\": false }\n // \"memoryExtraParams\": {},\n\n // Language for auto-capture summaries (default: \"auto\" for auto-detection)\n // Options: \"auto\", \"en\", \"id\", \"zh\", \"ja\", \"es\", \"fr\", \"de\", \"ru\", \"pt\", \"ar\", \"ko\"\n // \"autoCaptureLanguage\": \"auto\",\n\n // ============================================\n // Toast Notifications\n // ============================================\n\n // Show toast when memory is auto-captured\n \"showAutoCaptureToasts\": true,\n\n // Show toast when user profile is updated\n \"showUserProfileToasts\": true,\n\n // Show toast for error messages\n \"showErrorToasts\": true,\n\n // ============================================\n // User Profile System\n // ============================================\n\n // Analyze user prompts every N prompts to build/update your user profile\n // When N uncaptured prompts accumulate, AI will analyze them to identify:\n // - User preferences (code style, communication style, tool preferences)\n // - User patterns (recurring topics, problem domains, technical interests)\n // - User workflows (development habits, sequences, learning style)\n // - Skill level (overall and per-domain assessment)\n \"userProfileAnalysisInterval\": 10,\n\n // Days before inactive items (all types) are eligible for removal\n \"userProfileStaleDays\": 2,\n\n // Days before an inactive workflow is eligible for removal. A workflow seen\n // userProfileMinEvidenceForRetention times or more is kept.\n \"userProfileWorkflowStaleDays\": 30,\n\n // Number of preferences shown in UI\n \"userProfileDisplayPreferences\": 20,\n \n // Number of patterns shown in UI\n \"userProfileDisplayPatterns\": 15,\n \n // Number of workflows shown in UI\n \"userProfileDisplayWorkflows\": 10,\n \n // Number of preferences injected into LLM conversation context\n // Keep this small \u2014 the strongest signals are enough; more dilute LLM attention\n \"userProfileInjectPreferences\": 5,\n \n // Number of patterns injected into LLM conversation context\n \"userProfileInjectPatterns\": 5,\n \n // Number of workflows injected into LLM conversation context\n \"userProfileInjectWorkflows\": 3,\n \n // Days before preference confidence starts to decay (if not reinforced)\n // Preferences that aren't seen again will gradually lose confidence and be removed\n \"userProfileConfidenceDecayDays\": 30,\n \n // Number of profile versions to keep in changelog (for rollback/debugging)\n // Older versions are automatically cleaned up\n \"userProfileChangelogRetentionCount\": 5,\n\n // Minimum evidence count for a preference/pattern to survive confidence decay\n // Items confirmed fewer times are more likely to be pruned when confidence decays\n \"userProfileMinEvidenceForRetention\": 3,\n\n // Merge duplicate or vague profile items every N analysed prompts (about 1 model call per 100 items)\n \"userProfileAutoCleanupEnabled\": true,\n \"userProfileAutoCleanupInterval\": 500,\n\n // Enable LLM validation of existing preferences against recent behavior.\n // When enabled, each analysis round checks if top-5 preferences still match recent prompts.\n // Experimental \u2014 disabled by default.\n \"userProfileValidationEnabled\": false,\n\n // ============================================\n // Search Settings\n // ============================================\n \n // Minimum similarity score (0-1) for memory search results. Leave unset to\n // follow the default for your embedding model: 0.5 for qwen3-embedding:0.6b\n // (the only model measured), 0.6 for every other embedder.\n // \"similarityThreshold\": 0.5,\n\n // Maximum number of memories to return in search results.\n // Manual searches can ask for fewer; prompt retrieval uses this ceiling.\n \"maxMemories\": 10,\n\n // ============================================\n // Memory Context Controls\n // ============================================\n\n // Recent memories added at fresh session start (OpenCode V1 and Claude Code).\n // Pi and OpenCode V2 search the store with each prompt instead.\n \"chatMessage\": {\n \"enabled\": true,\n // Recent memories injected at session start.\n \"maxMemories\": 3,\n \"excludeCurrentSession\": true,\n // When to inject: \"first\" message of a session or \"always\".\n // \"injectOn\": \"first\",\n },\n\n // OpenCode profile-learning input limit, in UTF-8 bytes. Smaller values can\n // omit prompts from that input. Does not limit Pi or Claude Code profile input.\n \"userProfileMaxContextBytes\": 32768,\n\n // Shared budget for automatic memory context (profile text, memories, and\n // formatting) across all hosts, in approximate tokens: ceil(UTF-8 bytes / 4).\n // Covers prompt retrieval, recent-memory injection, and session-memory\n // restoration. Not an exact provider token limit or a spending limit.\n \"retrievalMaxTokens\": 2000,\n\n // Restore a session's own memories after compaction and after resume, on\n // OpenCode V2, Pi and Claude Code. memoryLimit is the most memories to\n // restore. A project config can turn it on or off.\n // \"compaction\": { \"enabled\": true, \"memoryLimit\": 10 },\n\n // ============================================\n // Advanced Settings\n // ============================================\n \n // Inject user profile into AI context (preferences, patterns, workflows)\n \"injectProfile\": true\n}\n";
@@ -254,6 +254,21 @@ export const CONFIG_TEMPLATE = `{
254
254
  // only. Unset: ~/.agents/skills and the Claude Code folder's skills folder.
255
255
  // "skillFolders": ["~/.agents/skills"],
256
256
 
257
+ // Visible recall: the OpenCode terminal TUI and Pi's interactive terminal draw
258
+ // a row for each memory recall. On by default. A project config can turn it
259
+ // off but never on.
260
+ // "visibleRecall": true,
261
+
262
+ // Memory injection, on OpenCode V2, Pi and Claude Code. Each prompt gets the
263
+ // memories that match it. openingContext also searches memory with
264
+ // openingContextQuery and adds the matches to the
265
+ // first message of a new session. The query is global only, trimmed, and
266
+ // 500 characters at most. subagentMemory gives each subagent the memories
267
+ // that match its task. For both switches, a project config can only turn these off.
268
+ // "openingContext": false,
269
+ // "openingContextQuery": "project architecture, setup, conventions and key decisions",
270
+ // "subagentMemory": true,
271
+
257
272
  // Capture retry queue: when a live capture fails because the model cannot be
258
273
  // reached (network error, timeout, HTTP 408, 429 or 5xx), OMMS keeps a copy of
259
274
  // the turn in ~/.omms/data and tries it again later. The copy has <private>
@@ -387,6 +402,11 @@ export const CONFIG_TEMPLATE = `{
387
402
  // restoration. Not an exact provider token limit or a spending limit.
388
403
  "retrievalMaxTokens": 2000,
389
404
 
405
+ // Restore a session's own memories after compaction and after resume, on
406
+ // OpenCode V2, Pi and Claude Code. memoryLimit is the most memories to
407
+ // restore. A project config can turn it on or off.
408
+ // "compaction": { "enabled": true, "memoryLimit": 10 },
409
+
390
410
  // ============================================
391
411
  // Advanced Settings
392
412
  // ============================================
package/dist/config.d.ts CHANGED
@@ -188,6 +188,10 @@ declare function buildConfig(fileConfig: OmmsConfig): {
188
188
  maxAgeDays: number | undefined;
189
189
  injectOn: "first" | "always";
190
190
  };
191
+ openingContext: boolean;
192
+ openingContextQuery: string;
193
+ subagentMemory: boolean;
194
+ visibleRecall: boolean;
191
195
  skillLearning: boolean;
192
196
  skillLearningRedact: boolean;
193
197
  skillFolders: string[] | undefined;
@@ -298,6 +302,10 @@ export declare let CONFIG: {
298
302
  maxAgeDays: number | undefined;
299
303
  injectOn: "first" | "always";
300
304
  };
305
+ openingContext: boolean;
306
+ openingContextQuery: string;
307
+ subagentMemory: boolean;
308
+ visibleRecall: boolean;
301
309
  skillLearning: boolean;
302
310
  skillLearningRedact: boolean;
303
311
  skillFolders: string[] | undefined;
package/dist/config.js CHANGED
@@ -10,6 +10,8 @@ import { getAutoCaptureProviderStatus } from "./services/ai/live-model-choice.js
10
10
  import { parseBackfillModel } from "./importer/backfill-model.js";
11
11
  import { parseIgnoredDirectories, parseImportPathMaps } from "./importer/import-path-maps.js";
12
12
  import { normaliseSkillConfig, projectSkillOverride, } from "./services/skill-learning/skill-config.js";
13
+ import * as visibleRecall from "./services/visible-recall-config.js";
14
+ import * as injectionOptions from "./services/injection-options-config.js";
13
15
  import { checkedMemoryLimit, MEMORY_LIMITS } from "./utils/memory-limits.js";
14
16
  import { defaultSimilarityThreshold } from "./utils/threshold-default.js";
15
17
  import { RERANKER_TIMEOUT_DEFAULT_MS } from "./services/request-timeouts.js";
@@ -30,12 +32,10 @@ const CONFIG_FILES = [
30
32
  join(OMMS_CONFIG_DIR, "omms.json"),
31
33
  ...LEGACY_CONFIG_FILES,
32
34
  ];
33
- if (!existsSync(OMMS_CONFIG_DIR)) {
35
+ if (!existsSync(OMMS_CONFIG_DIR))
34
36
  mkdirSync(OMMS_CONFIG_DIR, { recursive: true });
35
- }
36
- if (!existsSync(DATA_DIR)) {
37
+ if (!existsSync(DATA_DIR))
37
38
  mkdirSync(DATA_DIR, { recursive: true });
38
- }
39
39
  const DEFAULTS = {
40
40
  // Default store: ~/.omms/data (see resolveDefaultStoragePath for the
41
41
  // legacy-layout fallback while a legacy store exists unmigrated).
@@ -371,6 +371,8 @@ function buildConfig(fileConfig) {
371
371
  captureAttemptRetentionDays: normalizeAutoCleanupRetentionDays(fileConfig.captureAttemptRetentionDays ?? DEFAULTS.captureAttemptRetentionDays),
372
372
  captureRetryRetentionHours: normalizeCaptureRetryRetentionHours(fileConfig.captureRetryRetentionHours),
373
373
  ...normaliseSkillConfig(fileConfig),
374
+ ...visibleRecall.visibleRecallFromFile(fileConfig),
375
+ ...injectionOptions.injectionOptionsFromFile(fileConfig, log),
374
376
  autoBackfill: fileConfig.autoBackfill ?? DEFAULTS.autoBackfill,
375
377
  opencodeBackfillModel: fileConfig.opencodeBackfillModel ?? DEFAULTS.opencodeBackfillModel,
376
378
  piBackfillModel: fileConfig.piBackfillModel ?? DEFAULTS.piBackfillModel,
@@ -543,6 +545,8 @@ export function initConfig(directory, options = {}) {
543
545
  delete projectOverrides.skillLearningRedact;
544
546
  delete projectOverrides.skillFolders;
545
547
  Object.assign(projectOverrides, skillRule.override);
548
+ visibleRecall.applyProjectVisibleRecall(projectConfig, projectOverrides, directory, log);
549
+ injectionOptions.applyProjectInjectionOptions(projectConfig, projectOverrides, directory, log);
546
550
  delete projectOverrides.autoBackfill;
547
551
  delete projectOverrides.opencodeBackfillModel;
548
552
  delete projectOverrides.piBackfillModel;
@@ -59,6 +59,8 @@ export interface PackedSection {
59
59
  /** UTF-8 bytes of `text` (the host wrapper is not part of `text`). */
60
60
  bytesUsed: number;
61
61
  }
62
+ /** The byte ceiling of a budget: `maxBytes` when set, else `maxTokens` (default 2000) in bytes. */
63
+ export declare function resolveCeiling(options: ContextBudgetOptions): number;
62
64
  /**
63
65
  * Pack payload blocks into a section that fits one byte ceiling.
64
66
  *
@@ -16,7 +16,8 @@ export function estimateTokens(text) {
16
16
  export function tokensToByteCeiling(maxTokens) {
17
17
  return maxTokens * 4;
18
18
  }
19
- function resolveCeiling(options) {
19
+ /** The byte ceiling of a budget: `maxBytes` when set, else `maxTokens` (default 2000) in bytes. */
20
+ export function resolveCeiling(options) {
20
21
  if (options.maxBytes !== undefined)
21
22
  return options.maxBytes;
22
23
  return tokensToByteCeiling(options.maxTokens ?? DEFAULT_RETRIEVAL_MAX_TOKENS);
@@ -0,0 +1,45 @@
1
+ import { type ContextBudgetOptions } from "./context-budget.js";
2
+ /**
3
+ * The opening context (design D1c of change align-recall-host-parity): on the
4
+ * first prompt of a main session, one extra search with the configured query,
5
+ * added as a second retrieval section after the prompt's own. Shared by
6
+ * OpenCode V2, Pi and Claude Code. Hosts read `CONFIG` and pass the values in;
7
+ * this module does not import it, because many tests stub `src/config.js`.
8
+ */
9
+ /**
10
+ * The opening search query, or null when the option is off or the query is
11
+ * empty. Pass `CONFIG.openingContext` and `CONFIG.openingContextQuery`.
12
+ */
13
+ export declare function openingQueryFor(options: {
14
+ openingContext: boolean;
15
+ openingContextQuery: string;
16
+ }): string | null;
17
+ export interface FirstPromptSectionsInput {
18
+ prompt: string;
19
+ /** From `openingQueryFor`. Null runs no opening search. */
20
+ openingQuery: string | null;
21
+ directory: string;
22
+ sessionId: string;
23
+ /**
24
+ * The request budget, as `buildRetrievalSection` takes it. `wrapperBytes` is
25
+ * what the host adds around each section, the retrieval tag included; it
26
+ * defaults to the tag alone.
27
+ */
28
+ budget: ContextBudgetOptions;
29
+ /** Bytes the host adds to the opening section only, such as Claude Code's opening marker. */
30
+ openingReserveBytes: number;
31
+ }
32
+ /**
33
+ * The sections for the first prompt of a main session, each wrapped in the
34
+ * retrieval tag. The prompt section is `wrapRetrievalSection` of what
35
+ * `buildRetrievalSection` returns for the same budget, when the budget names
36
+ * its `wrapperBytes` as every host passes it. Both searches run in parallel with the same rules
37
+ * (`searchSectionMemories`). The opening section leaves out memories the
38
+ * prompt search found, carries no user profile (the prompt section holds it),
39
+ * and is packed with what the prompt section leaves of the budget, minus
40
+ * `openingReserveBytes`. Either section is null when nothing fits or matches.
41
+ */
42
+ export declare function buildFirstPromptSections(input: FirstPromptSectionsInput): Promise<{
43
+ promptSection: string | null;
44
+ openingSection: string | null;
45
+ }>;
@@ -0,0 +1,60 @@
1
+ import { formatContextForPrompt } from "../services/context.js";
2
+ import { getTags } from "../services/tags.js";
3
+ import { utf8ByteLength } from "../utils/context-limit.js";
4
+ import { resolveCeiling } from "./context-budget.js";
5
+ import { retrievalWrapperBytes, searchablePrompt, searchSectionMemories, wrapRetrievalSection, } from "./retrieval.js";
6
+ /**
7
+ * The opening context (design D1c of change align-recall-host-parity): on the
8
+ * first prompt of a main session, one extra search with the configured query,
9
+ * added as a second retrieval section after the prompt's own. Shared by
10
+ * OpenCode V2, Pi and Claude Code. Hosts read `CONFIG` and pass the values in;
11
+ * this module does not import it, because many tests stub `src/config.js`.
12
+ */
13
+ /**
14
+ * The opening search query, or null when the option is off or the query is
15
+ * empty. Pass `CONFIG.openingContext` and `CONFIG.openingContextQuery`.
16
+ */
17
+ export function openingQueryFor(options) {
18
+ if (!options.openingContext)
19
+ return null;
20
+ return options.openingContextQuery.trim() || null;
21
+ }
22
+ /**
23
+ * The sections for the first prompt of a main session, each wrapped in the
24
+ * retrieval tag. The prompt section is `wrapRetrievalSection` of what
25
+ * `buildRetrievalSection` returns for the same budget, when the budget names
26
+ * its `wrapperBytes` as every host passes it. Both searches run in parallel with the same rules
27
+ * (`searchSectionMemories`). The opening section leaves out memories the
28
+ * prompt search found, carries no user profile (the prompt section holds it),
29
+ * and is packed with what the prompt section leaves of the budget, minus
30
+ * `openingReserveBytes`. Either section is null when nothing fits or matches.
31
+ */
32
+ export async function buildFirstPromptSections(input) {
33
+ // A fully private prompt gets no section at all, the opening section included.
34
+ const promptQuery = searchablePrompt(input.prompt);
35
+ if (!promptQuery)
36
+ return { promptSection: null, openingSection: null };
37
+ const tags = getTags(input.directory);
38
+ const [promptSearch, openingSearch] = await Promise.all([
39
+ searchSectionMemories(promptQuery, tags.project.tag, input.sessionId),
40
+ input.openingQuery
41
+ ? searchSectionMemories(input.openingQuery, tags.project.tag, input.sessionId)
42
+ : null,
43
+ ]);
44
+ const wrapperBytes = input.budget.wrapperBytes ?? retrievalWrapperBytes();
45
+ const promptResults = promptSearch?.results ?? [];
46
+ const promptText = promptResults.length > 0
47
+ ? await formatContextForPrompt(tags.user.userEmail || null, { results: promptResults }, { ...input.budget, wrapperBytes }, promptSearch?.queryVector)
48
+ : "";
49
+ const promptIds = new Set(promptResults.map((result) => result.id));
50
+ const openingResults = (openingSearch?.results ?? []).filter((result) => !promptIds.has(result.id));
51
+ const promptBytes = promptText ? utf8ByteLength(promptText) + wrapperBytes : 0;
52
+ const remaining = resolveCeiling(input.budget) - promptBytes - input.openingReserveBytes;
53
+ const openingText = openingResults.length > 0 && remaining > 0
54
+ ? await formatContextForPrompt(null, { results: openingResults }, { maxBytes: remaining, wrapperBytes })
55
+ : "";
56
+ return {
57
+ promptSection: promptText ? wrapRetrievalSection(promptText) : null,
58
+ openingSection: openingText ? wrapRetrievalSection(openingText) : null,
59
+ };
60
+ }