om-memory-system 3.5.0-next.34 → 3.5.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 (37) hide show
  1. package/dist/adapters/opencode/auto-capture-summary.js +9 -1
  2. package/dist/adapters/opencode/backfill-startup.d.ts +3 -0
  3. package/dist/adapters/opencode/backfill-startup.js +6 -0
  4. package/dist/adapters/opencode/opencode-diagnostics.js +2 -1
  5. package/dist/adapters/opencode/opencode-provider.js +13 -3
  6. package/dist/adapters/pi/capture.js +29 -5
  7. package/dist/adapters/pi/extension.js +18 -1
  8. package/dist/config.d.ts +7 -1
  9. package/dist/config.js +21 -1
  10. package/dist/core/capture-retry-policy.d.ts +36 -0
  11. package/dist/core/capture-retry-policy.js +77 -0
  12. package/dist/core/capture.d.ts +9 -0
  13. package/dist/core/capture.js +20 -0
  14. package/dist/core/host.d.ts +4 -0
  15. package/dist/importer/model-selection.js +12 -2
  16. package/dist/index.js +15 -1
  17. package/dist/services/ai/providers/anthropic-messages.js +6 -1
  18. package/dist/services/ai/providers/base-provider.d.ts +17 -0
  19. package/dist/services/ai/providers/base-provider.js +19 -0
  20. package/dist/services/ai/providers/google-gemini.js +11 -2
  21. package/dist/services/ai/providers/openai-chat-completion.js +6 -1
  22. package/dist/services/ai/providers/openai-responses.js +6 -1
  23. package/dist/services/auto-capture.d.ts +6 -0
  24. package/dist/services/auto-capture.js +36 -5
  25. package/dist/services/capture-retry-drain.d.ts +48 -0
  26. package/dist/services/capture-retry-drain.js +142 -0
  27. package/dist/services/capture-retry-queue.d.ts +57 -0
  28. package/dist/services/capture-retry-queue.js +205 -0
  29. package/dist/services/cleanup-service.js +9 -0
  30. package/dist/services/global-config-writer.js +4 -0
  31. package/dist/services/settings-snapshot.js +2 -0
  32. package/dist/services/web-server.js +33 -2
  33. package/dist/web/assets/index-D5MY5Kjs.css +2 -0
  34. package/dist/web/assets/{index-D5A9vMgC.js → index-DIy--9g_.js} +1 -1
  35. package/dist/web/index.html +2 -2
  36. package/package.json +1 -1
  37. package/dist/web/assets/index-DvZhF1BJ.css +0 -2
@@ -2,6 +2,7 @@ import { randomUUID } from "node:crypto";
2
2
  import { CONFIG } from "../../config.js";
3
3
  import { resolveOpencodeHostModel } from "../../services/ai/live-model-choice.js";
4
4
  import { buildBoundedSummaryPrompt } from "../../core/capture-context.js";
5
+ import { errorHttpStatus, errorRetryAfterMs } from "../../core/capture-retry-policy.js";
5
6
  import { classifyCaptureReply, normalizeStopReason, parseCaptureSummary, } from "../../core/extraction.js";
6
7
  import { log } from "../../services/logger.js";
7
8
  import { loadOpencodeProvider } from "./opencode-provider-loader.js";
@@ -98,6 +99,8 @@ export async function generateOpenCodeAutoCaptureSummary(request, notify) {
98
99
  catch (error) {
99
100
  opencodeProviderError = error;
100
101
  diagnostics.failureReason = hostModelFailureReason(error);
102
+ diagnostics.httpStatus = errorHttpStatus(error);
103
+ diagnostics.retryAfterMs = errorRetryAfterMs(error);
101
104
  log("auto-capture: opencode provider failed, falling back to external API", {
102
105
  error: String(error),
103
106
  });
@@ -122,6 +125,7 @@ export async function generateOpenCodeAutoCaptureSummary(request, notify) {
122
125
  }
123
126
  const { AIProviderFactory } = await import("../../services/ai/ai-provider-factory.js");
124
127
  const { buildMemoryProviderConfig } = await import("../../services/ai/provider-config.js");
128
+ const { toolCallFailureReason } = await import("../../services/ai/providers/base-provider.js");
125
129
  const { detectLanguage, getLanguageName } = await import("../../services/language-detector.js");
126
130
  const providerConfig = buildMemoryProviderConfig(CONFIG);
127
131
  const provider = AIProviderFactory.createProvider(CONFIG.memoryProvider, providerConfig);
@@ -168,6 +172,8 @@ export async function generateOpenCodeAutoCaptureSummary(request, notify) {
168
172
  blockTypes: undefined,
169
173
  rawReply: undefined,
170
174
  failureReason: undefined,
175
+ httpStatus: undefined,
176
+ retryAfterMs: undefined,
171
177
  });
172
178
  let result;
173
179
  try {
@@ -178,8 +184,10 @@ export async function generateOpenCodeAutoCaptureSummary(request, notify) {
178
184
  throw error;
179
185
  }
180
186
  diagnostics.stopReason = normalizeStopReason(result.stopReason);
187
+ diagnostics.httpStatus = result.httpStatus;
188
+ diagnostics.retryAfterMs = result.retryAfterMs;
181
189
  if (!result.success || !result.data) {
182
- diagnostics.failureReason = diagnostics.stopReason === "length" ? "truncated" : "call-error";
190
+ diagnostics.failureReason = toolCallFailureReason(result, diagnostics.stopReason === "length");
183
191
  throw new Error(result.error || "Failed to generate summary");
184
192
  }
185
193
  const rawReply = JSON.stringify(result.data);
@@ -1,3 +1,4 @@
1
+ import type { AutoCaptureHost } from "../../core/host.js";
1
2
  /** Start after connected providers load, without delaying plugin initialisation. */
2
3
  export declare function startOpencodeBackfill(input: {
3
4
  directory: string;
@@ -13,3 +14,5 @@ export declare function registerOpencodeBackfillModels(input: {
13
14
  }): void;
14
15
  /** Let web imports, Health, and Settings in this process use OpenCode's signed-in models. */
15
16
  export declare function registerOpencodeImportModels(): void;
17
+ /** Let triggers and Retry now in this process retry OpenCode's queued turns. */
18
+ export declare function registerOpencodeCaptureRetryDrain(host: AutoCaptureHost, directory: string): void;
@@ -2,6 +2,8 @@ import { CONFIG } from "../../config.js";
2
2
  import { scheduleAutoBackfill } from "../../importer/auto-backfill.js";
3
3
  import { registerHostBackfillModels, registerOpencodeHostModels, } from "../../importer/backfill-controls.js";
4
4
  import { listOpencodeClientModels } from "../../importer/settings-models.js";
5
+ import { drainOpencodeCaptureRetries } from "../../services/auto-capture.js";
6
+ import { registerCaptureRetryDrain } from "../../services/capture-retry-drain.js";
5
7
  import { resolveOpencodeBackfillModels } from "./backfill-models.js";
6
8
  import { createOpencodeImportModels } from "./opencode-import-models.js";
7
9
  import { getV2Client, isProviderConnected } from "./opencode-provider.js";
@@ -29,3 +31,7 @@ export function registerOpencodeImportModels() {
29
31
  },
30
32
  });
31
33
  }
34
+ /** Let triggers and Retry now in this process retry OpenCode's queued turns. */
35
+ export function registerOpencodeCaptureRetryDrain(host, directory) {
36
+ registerCaptureRetryDrain("opencode", () => drainOpencodeCaptureRetries(host, directory));
37
+ }
@@ -1,3 +1,4 @@
1
+ import { httpStatusError, parseRetryAfter } from "../../core/capture-retry-policy.js";
1
2
  export function diagnosticUrl(url) {
2
3
  try {
3
4
  const parsed = new URL(url);
@@ -22,7 +23,7 @@ export async function readJson(res, endpoint) {
22
23
  const text = await res.text();
23
24
  const url = diagnosticUrl(endpoint.url);
24
25
  if (!res.ok) {
25
- throw new Error(`omms: opencode ${endpoint.label} failed at ${url} (${responseStatus(res)}): ${redactedBody(text)}`);
26
+ throw httpStatusError(`omms: opencode ${endpoint.label} failed at ${url} (${responseStatus(res)}): ${redactedBody(text)}`, res.status, parseRetryAfter(res.headers?.get?.("retry-after")));
26
27
  }
27
28
  if (!text) {
28
29
  throw new Error(`omms: opencode ${endpoint.label} at ${url} returned an empty response body`);
@@ -25,6 +25,7 @@ import { join } from "node:path";
25
25
  import { diagnosticUrl, readJson, responseStatus, } from "./opencode-diagnostics.js";
26
26
  import { INTERNAL_CAPTURE_SESSION_TITLE, trackInternalCaptureSession, untrackInternalCaptureSession, } from "./internal-capture-sessions.js";
27
27
  import { createLazyV2Client } from "./opencode-sdk-client.js";
28
+ import { httpStatusError, parseRetryAfter } from "../../core/capture-retry-policy.js";
28
29
  /** Dedicated agent registered via the plugin config hook (step-capped). */
29
30
  export const STRUCTURED_OUTPUT_AGENT = "omms-structured";
30
31
  /** Hard ceiling for a single internal structured-output prompt. */
@@ -227,7 +228,7 @@ export async function generateStructuredOutput(opts) {
227
228
  reportReply(onReply, reply);
228
229
  const info = reply.info;
229
230
  if (info.error) {
230
- throw new Error(`omms: opencode reported ${info.error.name}: ${formatAssistantError(info.error)}`);
231
+ throw assistantError(info.error);
231
232
  }
232
233
  const structuredOutput = info.structured_output ?? info.structured;
233
234
  if (structuredOutput === undefined || structuredOutput === null) {
@@ -286,7 +287,7 @@ async function generateViaSdkClient(client, args) {
286
287
  }
287
288
  reportReply(args.onReply, data);
288
289
  if (data.info.error) {
289
- throw new Error(`omms: opencode reported ${data.info.error.name}: ${formatAssistantError(data.info.error)}`);
290
+ throw assistantError(data.info.error);
290
291
  }
291
292
  const structuredOutput = data.info.structured_output ?? data.info.structured;
292
293
  if (structuredOutput === undefined || structuredOutput === null) {
@@ -343,7 +344,10 @@ function readSdkData(response, label) {
343
344
  const status = result.response ? ` (${responseStatus(result.response)})` : "";
344
345
  const responseUrl = result.response?.url || result.request?.url;
345
346
  const url = responseUrl ? diagnosticUrl(responseUrl) : "the authenticated client";
346
- throw new Error(`omms: opencode ${label} failed at ${url}${status}: <redacted response body>`);
347
+ const message = `omms: opencode ${label} failed at ${url}${status}: <redacted response body>`;
348
+ if (!result.response)
349
+ throw new Error(message);
350
+ throw httpStatusError(message, result.response.status, parseRetryAfter(result.response.headers?.get?.("retry-after")));
347
351
  }
348
352
  if (result?.data === undefined) {
349
353
  throw new Error(`omms: opencode ${label} returned no response data`);
@@ -382,6 +386,12 @@ async function createSession(base, directory) {
382
386
  trackInternalCaptureSession(body.id);
383
387
  return body.id;
384
388
  }
389
+ /** The model's error reply, with its HTTP status when OpenCode reports one. */
390
+ function assistantError(error) {
391
+ const message = `omms: opencode reported ${error.name}: ${formatAssistantError(error)}`;
392
+ const status = error.data?.statusCode;
393
+ return typeof status === "number" ? httpStatusError(message, status) : new Error(message);
394
+ }
385
395
  function formatAssistantError(error) {
386
396
  if (!error.data)
387
397
  return error.name;
@@ -1,11 +1,30 @@
1
1
  import { CONFIG } from "../../config.js";
2
2
  import { captureConversation } from "../../core/capture.js";
3
+ import { quickRetryDelayMs } from "../../core/capture-retry-policy.js";
3
4
  import { log } from "../../services/logger.js";
4
5
  import { memoryClient } from "../../services/client.js";
6
+ import { queueFailedCapture, startCaptureRetryDrain } from "../../services/capture-retry-drain.js";
5
7
  import { extractPiConversation } from "../../importer/pi-conversation.js";
6
8
  export function createPiCaptureState() {
7
9
  return { handledUserEntries: new Set(), running: false };
8
10
  }
11
+ /** Try the turn up to `autoCaptureMaxRetries` times, as OpenCode does, before it counts as failed. */
12
+ async function captureWithQuickRetries(unit, provider) {
13
+ const maxRetries = Math.max(1, CONFIG.autoCaptureMaxRetries ?? 3);
14
+ for (let attempt = 1;; attempt++) {
15
+ try {
16
+ return await captureConversation(unit, provider);
17
+ }
18
+ catch (error) {
19
+ if (attempt >= maxRetries)
20
+ throw error;
21
+ log(`Pi auto-capture warning (attempt ${attempt}/${maxRetries})`, {
22
+ sessionID: unit.hostSessionId,
23
+ });
24
+ await new Promise((resolve) => setTimeout(resolve, quickRetryDelayMs(attempt)));
25
+ }
26
+ }
27
+ }
9
28
  /**
10
29
  * Capture the work unit completed by the most recent user prompt on the
11
30
  * settled branch. Safe to call for every `agent_settled` event: a work unit is
@@ -18,6 +37,7 @@ export async function capturePiSettledWorkUnit(input) {
18
37
  if (input.state.running)
19
38
  return { status: "skipped", reason: "busy" };
20
39
  input.state.running = true;
40
+ let workUnit = null;
21
41
  try {
22
42
  const window = extractPiConversation(input.entries);
23
43
  if (!window)
@@ -29,7 +49,7 @@ export async function capturePiSettledWorkUnit(input) {
29
49
  if (embeddingInitError) {
30
50
  return { status: "skipped", reason: "not-ready" };
31
51
  }
32
- const captureResult = await captureConversation({
52
+ workUnit = {
33
53
  host: "pi",
34
54
  hostSessionId: input.sessionId,
35
55
  sourceType: "live-capture",
@@ -44,10 +64,9 @@ export async function capturePiSettledWorkUnit(input) {
44
64
  textResponses: window.textResponses,
45
65
  toolCalls: window.toolCalls,
46
66
  sourceEntryIds: window.sourceEntryIds,
47
- ...(window.sourceTimestamp !== undefined
48
- ? { sourceTimestamp: window.sourceTimestamp }
49
- : {}),
50
- }, input.provider);
67
+ ...(window.sourceTimestamp !== undefined ? { sourceTimestamp: window.sourceTimestamp } : {}),
68
+ };
69
+ const captureResult = await captureWithQuickRetries(workUnit, input.provider);
51
70
  input.state.handledUserEntries.add(window.userEntryId);
52
71
  if (captureResult.status === "skipped") {
53
72
  log("Pi auto-capture skipped by extractor", {
@@ -63,6 +82,8 @@ export async function capturePiSettledWorkUnit(input) {
63
82
  memoryId: captureResult.memoryId,
64
83
  host: "pi",
65
84
  });
85
+ // The capture model answers again, so earlier failed turns may go through now.
86
+ startCaptureRetryDrain("pi");
66
87
  if (CONFIG.showAutoCaptureToasts && input.notify) {
67
88
  await Promise.resolve(input.notify({
68
89
  title: "Memory Captured",
@@ -75,6 +96,9 @@ export async function capturePiSettledWorkUnit(input) {
75
96
  catch (error) {
76
97
  const message = error instanceof Error ? error.message : String(error);
77
98
  log(`Pi auto-capture error: ${message}`, { sessionID: input.sessionId });
99
+ // The next prompt moves capture on to a newer turn, so keep this one for a later retry.
100
+ if (workUnit)
101
+ await queueFailedCapture(workUnit, error, CONFIG);
78
102
  if (CONFIG.showErrorToasts && input.notify) {
79
103
  const shortReason = message.length > 100 ? message.substring(0, 100) + "..." : message;
80
104
  await Promise.resolve(input.notify({
@@ -1,5 +1,5 @@
1
1
  import { Type } from "typebox";
2
- import { CONFIG, initConfigWithLegacyMigration, isConfigured } from "../../config.js";
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
5
  import { getLanguageName } from "../../services/language-detector.js";
@@ -111,6 +111,23 @@ export default function ommsPiExtension(pi) {
111
111
  code: error instanceof Error ? error.name : "unknown",
112
112
  }));
113
113
  }
114
+ // Retry turns whose capture failed earlier, with this session's capture model.
115
+ void import("../../services/capture-retry-drain.js")
116
+ .then(({ drainCaptureRetries, registerCaptureRetryDrain, startCaptureRetryDrain }) => {
117
+ registerCaptureRetryDrain("pi", () => {
118
+ // A retention change in the config file applies from this pass on.
119
+ refreshConfigIfChanged(ctx.cwd);
120
+ return drainCaptureRetries({
121
+ host: "pi",
122
+ provider: createPiLiveModels(ctx, notify(ctx)).capture,
123
+ config: CONFIG,
124
+ isReady: () => isConfigured() && !memoryClient.getEmbeddingInitError?.(),
125
+ });
126
+ });
127
+ if (isConfigured() && CONFIG.autoCaptureEnabled)
128
+ startCaptureRetryDrain("pi");
129
+ })
130
+ .catch(() => { });
114
131
  // Run now on a Settings page served by this process can use Pi's signed-in models.
115
132
  void Promise.all([
116
133
  import("../../importer/backfill-controls.js"),
package/dist/config.d.ts CHANGED
@@ -42,6 +42,8 @@ interface OmmsConfig {
42
42
  captureTrace?: boolean;
43
43
  captureTraceRetentionDays?: number;
44
44
  captureAttemptRetentionDays?: number;
45
+ /** Hours to keep failed live captures for retry. 0 turns the queue off. Global config only. */
46
+ captureRetryRetentionHours?: number;
45
47
  autoBackfill?: boolean;
46
48
  opencodeBackfillModel?: string;
47
49
  piBackfillModel?: string;
@@ -98,8 +100,10 @@ interface OmmsConfig {
98
100
  injectOn?: "first" | "always";
99
101
  };
100
102
  }
101
- 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\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 // Maximum number of times to retry capturing a prompt if it fails (due to network, API errors, etc.)\n \"autoCaptureMaxRetries\": 3,\n\n // Maximum UTF-8 bytes for the auto-capture markdown context sent to the summary model.\n // Prevents HTTP 400 context overflows on models with ~131K token windows (e.g. Groq Llama).\n // Rough guide: tokens \u2248 bytes / 4 for mixed code/prose.\n \"autoCaptureMaxContextBytes\": 131072,\n \n // Days to keep AI session history before cleanup\n \"aiSessionRetentionDays\": 7,\n\n // Capture diagnostics: every capture attempt always writes one metadata line\n // (model, stop reason, sizes, outcome) to ~/.omms/omms.log, with no\n // conversation text. Set captureTrace to true to also write each attempt's\n // prompt and raw reply to ~/.omms/traces/ for debugging. Traces can contain\n // conversation content: <private> text and common API key formats are\n // redacted, files are readable only by you, and they are deleted after\n // captureTraceRetentionDays. Only this global file can turn tracing on.\n // \"captureTrace\": false,\n // \"captureTraceRetentionDays\": 7,\n // \"captureAttemptRetentionDays\": 30,\n\n // Temperature for AI API requests (set to false to omit parameter for models that don't support it)\n // Some reasoning models (like o1, o3, gpt-5) don't support temperature parameter\n // Set to false and add \"memoryTemperature\": false in config when using such models\n \"memoryTemperature\": 0.3,\n\n // Extra parameters to include in API request body\n // Useful for local inference servers (e.g. llama-server with --jinja) that support\n // additional parameters like disabling thinking/reasoning mode\n // Example for Qwen3 models: { \"enable_thinking\": false }\n // \"memoryExtraParams\": {},\n\n // Language for auto-capture summaries (default: \"auto\" for auto-detection)\n // Options: \"auto\", \"en\", \"id\", \"zh\", \"ja\", \"es\", \"fr\", \"de\", \"ru\", \"pt\", \"ar\", \"ko\"\n // \"autoCaptureLanguage\": \"auto\",\n\n // ============================================\n // Toast Notifications\n // ============================================\n\n // Show toast when memory is auto-captured\n \"showAutoCaptureToasts\": true,\n\n // Show toast when user profile is updated\n \"showUserProfileToasts\": true,\n\n // Show toast for error messages\n \"showErrorToasts\": true,\n\n // ============================================\n // User Profile System\n // ============================================\n\n // Analyze user prompts every N prompts to build/update your user profile\n // When N uncaptured prompts accumulate, AI will analyze them to identify:\n // - User preferences (code style, communication style, tool preferences)\n // - User patterns (recurring topics, problem domains, technical interests)\n // - User workflows (development habits, sequences, learning style)\n // - Skill level (overall and per-domain assessment)\n \"userProfileAnalysisInterval\": 10,\n\n // Days before inactive items (all types) are eligible for removal\n \"userProfileStaleDays\": 2,\n\n // Number of preferences shown in UI\n \"userProfileDisplayPreferences\": 20,\n \n // Number of patterns shown in UI\n \"userProfileDisplayPatterns\": 15,\n \n // Number of workflows shown in UI\n \"userProfileDisplayWorkflows\": 10,\n \n // Number of preferences injected into LLM conversation context\n // Keep this small \u2014 the strongest signals are enough; more dilute LLM attention\n \"userProfileInjectPreferences\": 5,\n \n // Number of patterns injected into LLM conversation context\n \"userProfileInjectPatterns\": 5,\n \n // Number of workflows injected into LLM conversation context\n \"userProfileInjectWorkflows\": 3,\n \n // Days before preference confidence starts to decay (if not reinforced)\n // Preferences that aren't seen again will gradually lose confidence and be removed\n \"userProfileConfidenceDecayDays\": 30,\n \n // Number of profile versions to keep in changelog (for rollback/debugging)\n // Older versions are automatically cleaned up\n \"userProfileChangelogRetentionCount\": 5,\n\n // Minimum evidence count for a preference/pattern to survive confidence decay\n // Items confirmed fewer times are more likely to be pruned when confidence decays\n \"userProfileMinEvidenceForRetention\": 3,\n\n // Periodically merge duplicate or irrelevant profile items with the configured AI provider\n \"userProfileAutoCleanupEnabled\": true,\n // Number of analyzed user prompts between automatic AI cleanup runs\n \"userProfileAutoCleanupInterval\": 100,\n\n // Enable LLM validation of existing preferences against recent behavior.\n // When enabled, each analysis round checks if top-5 preferences still match recent prompts.\n // Experimental \u2014 disabled by default.\n \"userProfileValidationEnabled\": false,\n\n // ============================================\n // Search Settings\n // ============================================\n \n // Minimum similarity score (0-1) for memory search results\n \"similarityThreshold\": 0.6,\n\n // Maximum number of memories to return in search results\n \"maxMemories\": 10,\n\n // ============================================\n // Advanced Settings\n // ============================================\n \n // Inject user profile into AI context (preferences, patterns, workflows)\n \"injectProfile\": true\n}\n";
103
+ 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\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";
102
104
  export declare function normalizeAutoCaptureMaxContextBytes(value: number): number;
105
+ /** Whole hours from 0 to 720. A value that is not a number gives the default. */
106
+ export declare function normalizeCaptureRetryRetentionHours(value: unknown): number;
103
107
  export declare function normalizeAutoCleanupRetentionDays(value: number): number;
104
108
  declare function buildConfig(fileConfig: OmmsConfig): {
105
109
  storagePath: string;
@@ -136,6 +140,7 @@ declare function buildConfig(fileConfig: OmmsConfig): {
136
140
  captureTrace: boolean;
137
141
  captureTraceRetentionDays: number;
138
142
  captureAttemptRetentionDays: number;
143
+ captureRetryRetentionHours: number;
139
144
  autoBackfill: boolean;
140
145
  opencodeBackfillModel: string;
141
146
  piBackfillModel: string;
@@ -229,6 +234,7 @@ export declare let CONFIG: {
229
234
  captureTrace: boolean;
230
235
  captureTraceRetentionDays: number;
231
236
  captureAttemptRetentionDays: number;
237
+ captureRetryRetentionHours: number;
232
238
  autoBackfill: boolean;
233
239
  opencodeBackfillModel: string;
234
240
  piBackfillModel: string;
package/dist/config.js CHANGED
@@ -53,6 +53,7 @@ const DEFAULTS = {
53
53
  captureTrace: false,
54
54
  captureTraceRetentionDays: 7,
55
55
  captureAttemptRetentionDays: 30,
56
+ captureRetryRetentionHours: 72,
56
57
  autoBackfill: true,
57
58
  opencodeBackfillModel: "inherit",
58
59
  piBackfillModel: "inherit",
@@ -365,7 +366,8 @@ export const CONFIG_TEMPLATE = `{
365
366
  // Timeout per iteration in milliseconds (30 seconds default)
366
367
  "autoCaptureIterationTimeout": 30000,
367
368
 
368
- // Maximum number of times to retry capturing a prompt if it fails (due to network, API errors, etc.)
369
+ // Tries for one turn's capture, on Pi and OpenCode, 2 and 4 seconds apart. After the
370
+ // last one fails, a turn the model could not reach goes to the capture retry queue.
369
371
  "autoCaptureMaxRetries": 3,
370
372
 
371
373
  // Maximum UTF-8 bytes for the auto-capture markdown context sent to the summary model.
@@ -387,6 +389,14 @@ export const CONFIG_TEMPLATE = `{
387
389
  // "captureTraceRetentionDays": 7,
388
390
  // "captureAttemptRetentionDays": 30,
389
391
 
392
+ // Capture retry queue: when a live capture fails because the model cannot be
393
+ // reached (network error, timeout, HTTP 408, 429 or 5xx), OMMS keeps a copy of
394
+ // the turn in ~/.omms/data and tries it again later. The copy has <private>
395
+ // text and common API key formats removed. Turns are deleted when the retry
396
+ // ends or after captureRetryRetentionHours (0 to 720). 0 turns the queue off.
397
+ // Only this global file can set it.
398
+ // "captureRetryRetentionHours": 72,
399
+
390
400
  // Temperature for AI API requests (set to false to omit parameter for models that don't support it)
391
401
  // Some reasoning models (like o1, o3, gpt-5) don't support temperature parameter
392
402
  // Set to false and add "memoryTemperature": false in config when using such models
@@ -553,6 +563,13 @@ export function normalizeAutoCaptureMaxContextBytes(value) {
553
563
  }
554
564
  return value;
555
565
  }
566
+ /** Whole hours from 0 to 720. A value that is not a number gives the default. */
567
+ export function normalizeCaptureRetryRetentionHours(value) {
568
+ if (typeof value !== "number" || !Number.isFinite(value)) {
569
+ return DEFAULTS.captureRetryRetentionHours;
570
+ }
571
+ return Math.min(720, Math.max(0, Math.floor(value)));
572
+ }
556
573
  export function normalizeAutoCleanupRetentionDays(value) {
557
574
  if (!Number.isSafeInteger(value) || value <= 0) {
558
575
  throw new Error(`Invalid autoCleanupRetentionDays config: ${value}`);
@@ -641,6 +658,7 @@ function buildConfig(fileConfig) {
641
658
  captureTrace: fileConfig.captureTrace === true,
642
659
  captureTraceRetentionDays: Math.max(1, Math.floor(fileConfig.captureTraceRetentionDays ?? DEFAULTS.captureTraceRetentionDays)),
643
660
  captureAttemptRetentionDays: normalizeAutoCleanupRetentionDays(fileConfig.captureAttemptRetentionDays ?? DEFAULTS.captureAttemptRetentionDays),
661
+ captureRetryRetentionHours: normalizeCaptureRetryRetentionHours(fileConfig.captureRetryRetentionHours),
644
662
  autoBackfill: fileConfig.autoBackfill ?? DEFAULTS.autoBackfill,
645
663
  opencodeBackfillModel: fileConfig.opencodeBackfillModel ?? DEFAULTS.opencodeBackfillModel,
646
664
  piBackfillModel: fileConfig.piBackfillModel ?? DEFAULTS.piBackfillModel,
@@ -796,6 +814,8 @@ export function initConfig(directory, options = {}) {
796
814
  delete projectOverrides.captureTrace;
797
815
  }
798
816
  delete projectOverrides.captureTraceRetentionDays;
817
+ // The queue holds conversation text, so only the global file sets how long.
818
+ delete projectOverrides.captureRetryRetentionHours;
799
819
  delete projectOverrides.autoBackfill;
800
820
  delete projectOverrides.opencodeBackfillModel;
801
821
  delete projectOverrides.piBackfillModel;
@@ -0,0 +1,36 @@
1
+ import type { CaptureFailureReason } from "./host.js";
2
+ /** What the capture pipeline knows about a failed attempt. */
3
+ export interface CaptureFailureInfo {
4
+ reason: CaptureFailureReason;
5
+ httpStatus?: number;
6
+ retryAfterMs?: number;
7
+ }
8
+ export type CaptureRetryClass = "retryable" | "permanent";
9
+ /**
10
+ * A retry can fix a model call that never got an answer, a timeout, a rate
11
+ * limit, a server error, or a busy local store. Anything else would send the
12
+ * same input to the same model for the same bad result, and each try costs.
13
+ */
14
+ export declare function classifyCaptureFailure(failure: CaptureFailureInfo): CaptureRetryClass;
15
+ /** The wait inside a turn after quick try `attempt` fails: 2 s, then 4 s, doubling. */
16
+ export declare function quickRetryDelayMs(attempt: number): number;
17
+ /** Waits after the first to fourth failed tries; every later try waits 12 hours. */
18
+ export declare const CAPTURE_RETRY_WAITS_MS: readonly [60000, number, number, number];
19
+ export declare const CAPTURE_RETRY_LATER_WAIT_MS: number;
20
+ /**
21
+ * The wait before the next try, after `failedTries` failed tries (the live
22
+ * attempt counts as the first). Adds up to 20 % random extra wait, and never
23
+ * returns less than a `Retry-After` value the API sent.
24
+ */
25
+ export declare function captureRetryDelayMs(failedTries: number, retryAfterMs?: number, random?: () => number): number;
26
+ /** Read an HTTP `Retry-After` header: seconds or an HTTP date. */
27
+ export declare function parseRetryAfter(value: string | null | undefined, now?: number): number | undefined;
28
+ /** The HTTP status a provider attached to a thrown error, if any. */
29
+ export declare function errorHttpStatus(error: unknown): number | undefined;
30
+ /** The `Retry-After` wait a provider attached to a thrown error, if any. */
31
+ export declare function errorRetryAfterMs(error: unknown): number | undefined;
32
+ /** An error that carries the HTTP status of the reply that caused it. */
33
+ export declare function httpStatusError(message: string, httpStatus: number, retryAfterMs?: number): Error & {
34
+ httpStatus: number;
35
+ retryAfterMs?: number;
36
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * A retry can fix a model call that never got an answer, a timeout, a rate
3
+ * limit, a server error, or a busy local store. Anything else would send the
4
+ * same input to the same model for the same bad result, and each try costs.
5
+ */
6
+ export function classifyCaptureFailure(failure) {
7
+ if (failure.reason === "persist-error")
8
+ return "retryable";
9
+ if (failure.reason !== "call-error")
10
+ return "permanent";
11
+ const status = failure.httpStatus;
12
+ if (status === undefined)
13
+ return "retryable";
14
+ if (status === 408 || status === 429 || status >= 500)
15
+ return "retryable";
16
+ return "permanent";
17
+ }
18
+ const QUICK_RETRY_BASE_MS = 2000;
19
+ /** The wait inside a turn after quick try `attempt` fails: 2 s, then 4 s, doubling. */
20
+ export function quickRetryDelayMs(attempt) {
21
+ return QUICK_RETRY_BASE_MS * Math.pow(2, attempt - 1);
22
+ }
23
+ const MINUTE = 60_000;
24
+ const HOUR = 60 * MINUTE;
25
+ /** Waits after the first to fourth failed tries; every later try waits 12 hours. */
26
+ export const CAPTURE_RETRY_WAITS_MS = [MINUTE, 5 * MINUTE, 30 * MINUTE, 2 * HOUR];
27
+ export const CAPTURE_RETRY_LATER_WAIT_MS = 12 * HOUR;
28
+ const JITTER = 0.2;
29
+ /**
30
+ * The wait before the next try, after `failedTries` failed tries (the live
31
+ * attempt counts as the first). Adds up to 20 % random extra wait, and never
32
+ * returns less than a `Retry-After` value the API sent.
33
+ */
34
+ export function captureRetryDelayMs(failedTries, retryAfterMs, random = Math.random) {
35
+ const index = Math.max(1, Math.floor(failedTries)) - 1;
36
+ const base = CAPTURE_RETRY_WAITS_MS[index] ?? CAPTURE_RETRY_LATER_WAIT_MS;
37
+ const delay = Math.round(base * (1 + JITTER * random()));
38
+ return retryAfterMs !== undefined && retryAfterMs > delay ? retryAfterMs : delay;
39
+ }
40
+ /** Read an HTTP `Retry-After` header: seconds or an HTTP date. */
41
+ export function parseRetryAfter(value, now = Date.now()) {
42
+ if (!value)
43
+ return undefined;
44
+ const trimmed = value.trim();
45
+ if (/^\d+$/.test(trimmed))
46
+ return Number(trimmed) * 1000;
47
+ const date = Date.parse(trimmed);
48
+ if (Number.isNaN(date))
49
+ return undefined;
50
+ return Math.max(0, date - now);
51
+ }
52
+ function numberField(error, key) {
53
+ let current = error;
54
+ // Follow a short cause chain: providers wrap errors once or twice.
55
+ for (let depth = 0; depth < 4 && current && typeof current === "object"; depth++) {
56
+ const value = current[key];
57
+ if (typeof value === "number" && Number.isFinite(value))
58
+ return value;
59
+ current = current.cause;
60
+ }
61
+ return undefined;
62
+ }
63
+ /** The HTTP status a provider attached to a thrown error, if any. */
64
+ export function errorHttpStatus(error) {
65
+ return numberField(error, "httpStatus");
66
+ }
67
+ /** The `Retry-After` wait a provider attached to a thrown error, if any. */
68
+ export function errorRetryAfterMs(error) {
69
+ return numberField(error, "retryAfterMs");
70
+ }
71
+ /** An error that carries the HTTP status of the reply that caused it. */
72
+ export function httpStatusError(message, httpStatus, retryAfterMs) {
73
+ return Object.assign(new Error(message), {
74
+ httpStatus,
75
+ ...(retryAfterMs !== undefined ? { retryAfterMs } : {}),
76
+ });
77
+ }
@@ -1,3 +1,4 @@
1
+ import { type CaptureFailureInfo } from "./capture-retry-policy.js";
1
2
  import type { CaptureConversation, CapturePromptContext, CaptureProvenance, CaptureSummaryProvider } from "./host.js";
2
3
  export interface CaptureWorkUnit extends CaptureProvenance, CaptureConversation {
3
4
  projectDirectory: string;
@@ -12,6 +13,14 @@ export type CaptureResult = {
12
13
  status: "skipped";
13
14
  type?: string;
14
15
  };
16
+ /**
17
+ * Thrown by `captureConversation` for a failed attempt. Keeps the original
18
+ * message and cause, and adds what the retry queue needs to classify it.
19
+ */
20
+ export declare class CaptureAttemptError extends Error {
21
+ readonly failure: CaptureFailureInfo;
22
+ constructor(error: unknown, failure: CaptureFailureInfo);
23
+ }
15
24
  /**
16
25
  * Run one capture attempt and write exactly one diagnostics record for it,
17
26
  * whether it is saved, skipped, or fails.
@@ -3,6 +3,19 @@ import { buildCaptureAttemptRecord, emitCaptureAttempt } from "../services/captu
3
3
  import { memoryClient } from "../services/client.js";
4
4
  import { getTags } from "../services/tags.js";
5
5
  import { buildMarkdownContext, getAutoCaptureMarkdownBudget } from "./capture-context.js";
6
+ import { errorHttpStatus, errorRetryAfterMs, } from "./capture-retry-policy.js";
7
+ /**
8
+ * Thrown by `captureConversation` for a failed attempt. Keeps the original
9
+ * message and cause, and adds what the retry queue needs to classify it.
10
+ */
11
+ export class CaptureAttemptError extends Error {
12
+ failure;
13
+ constructor(error, failure) {
14
+ super(error instanceof Error ? error.message : String(error), { cause: error });
15
+ this.name = "CaptureAttemptError";
16
+ this.failure = failure;
17
+ }
18
+ }
6
19
  async function getLatestProjectMemory(containerTag) {
7
20
  try {
8
21
  const result = await memoryClient.listMemories(containerTag, 1);
@@ -31,6 +44,13 @@ export async function captureConversation(workUnit, provider) {
31
44
  outcome = result.status === "captured" ? "saved" : "skipped";
32
45
  return result;
33
46
  }
47
+ catch (error) {
48
+ throw new CaptureAttemptError(error, {
49
+ reason: diagnostics.failureReason ?? "call-error",
50
+ httpStatus: diagnostics.httpStatus ?? errorHttpStatus(error),
51
+ retryAfterMs: diagnostics.retryAfterMs ?? errorRetryAfterMs(error),
52
+ });
53
+ }
34
54
  finally {
35
55
  const record = buildCaptureAttemptRecord({
36
56
  host: workUnit.host,
@@ -40,6 +40,10 @@ export interface CaptureAttemptDiagnostics {
40
40
  userPrompt?: string;
41
41
  rawReply?: string;
42
42
  failureReason?: CaptureFailureReason;
43
+ /** HTTP status of a failed model call, when the path knows it. */
44
+ httpStatus?: number;
45
+ /** The wait a failed model call asked for in its `Retry-After` header. */
46
+ retryAfterMs?: number;
43
47
  }
44
48
  export interface CaptureSummaryRequest {
45
49
  context: string;