memorix 1.4.6 → 1.5.1

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 (119) hide show
  1. package/CHANGELOG.md +38 -0
  2. package/TEAM.md +86 -86
  3. package/dist/cli/index.js +225 -70
  4. package/dist/cli/memcode.js +0 -0
  5. package/dist/index.js +225 -70
  6. package/dist/maintenance-runner.js +183 -38
  7. package/dist/media-audio-runner.js +183 -38
  8. package/dist/media-video-runner.js +183 -38
  9. package/dist/memcode-runtime/CHANGELOG.md +38 -0
  10. package/dist/sdk.js +224 -69
  11. package/dist/types.d.ts +2 -0
  12. package/dist/vector-backfill-runner.js +183 -38
  13. package/docs/CONFIGURATION.md +12 -0
  14. package/docs/DESIGN_DECISIONS.md +357 -357
  15. package/package.json +1 -1
  16. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  17. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  18. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  19. package/plugins/copilot/memorix/plugin.json +1 -1
  20. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  21. package/plugins/omp/memorix/package.json +1 -1
  22. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  23. package/plugins/pi/memorix/package.json +1 -1
  24. package/server.json +2 -2
  25. package/src/audit/index.ts +156 -156
  26. package/src/cli/commands/formation.ts +48 -48
  27. package/src/cli/commands/git-hook-install.ts +111 -111
  28. package/src/cli/commands/handoff.ts +54 -54
  29. package/src/cli/commands/hooks-status.ts +63 -63
  30. package/src/cli/commands/ingest.ts +44 -44
  31. package/src/cli/commands/integrate-shared.ts +15 -15
  32. package/src/cli/commands/lock.ts +82 -82
  33. package/src/cli/commands/message.ts +104 -104
  34. package/src/cli/commands/poll.ts +58 -58
  35. package/src/cli/commands/reasoning.ts +118 -118
  36. package/src/cli/commands/skills.ts +114 -114
  37. package/src/cli/commands/task.ts +167 -167
  38. package/src/cli/commands/transfer.ts +47 -47
  39. package/src/cli/index.ts +406 -406
  40. package/src/cli/tui/ChatView.tsx +234 -234
  41. package/src/cli/tui/CommandBar.tsx +312 -312
  42. package/src/cli/tui/ContextRail.tsx +118 -118
  43. package/src/cli/tui/HeaderBar.tsx +72 -72
  44. package/src/cli/tui/LogoBanner.tsx +51 -51
  45. package/src/cli/tui/index.ts +41 -41
  46. package/src/cli/version.ts +7 -7
  47. package/src/cli/workbench.ts +1 -1
  48. package/src/config.ts +23 -2
  49. package/src/dashboard/project-classification.ts +64 -64
  50. package/src/embedding/api-provider.ts +105 -22
  51. package/src/embedding/provider.ts +2 -0
  52. package/src/git/hooks-path.ts +85 -85
  53. package/src/hooks/pattern-detector.ts +173 -173
  54. package/src/hooks/significance-filter.ts +250 -250
  55. package/src/llm/memory-manager.ts +328 -328
  56. package/src/llm/provider.ts +17 -8
  57. package/src/llm/quality.ts +248 -248
  58. package/src/memory/attribution-guard.ts +249 -249
  59. package/src/memory/disclosure-policy.ts +135 -135
  60. package/src/memory/entity-extractor.ts +197 -197
  61. package/src/memory/formation/evaluate.ts +217 -217
  62. package/src/memory/formation/extract.ts +6 -3
  63. package/src/memory/formation/index.ts +2 -1
  64. package/src/memory/formation/resolve.ts +10 -6
  65. package/src/memory/formation/types.ts +3 -1
  66. package/src/memory/freshness.ts +122 -122
  67. package/src/memory/graph.ts +197 -197
  68. package/src/memory/long-term.ts +7 -1
  69. package/src/memory/refs.ts +94 -94
  70. package/src/memory/secret-filter.ts +79 -79
  71. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  72. package/src/orchestrate/adapters/claude.ts +111 -111
  73. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  74. package/src/orchestrate/adapters/codex.ts +41 -41
  75. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  76. package/src/orchestrate/adapters/gemini.ts +42 -42
  77. package/src/orchestrate/adapters/index.ts +73 -73
  78. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  79. package/src/orchestrate/adapters/opencode.ts +47 -47
  80. package/src/orchestrate/adapters/types.ts +77 -77
  81. package/src/orchestrate/capability-router.ts +284 -284
  82. package/src/orchestrate/cost-tracker.ts +219 -219
  83. package/src/orchestrate/error-recovery.ts +191 -191
  84. package/src/orchestrate/evidence.ts +140 -140
  85. package/src/orchestrate/ledger.ts +110 -110
  86. package/src/orchestrate/memorix-bridge.ts +343 -343
  87. package/src/orchestrate/output-budget.ts +80 -80
  88. package/src/orchestrate/permission.ts +152 -152
  89. package/src/orchestrate/pipeline-trace.ts +131 -131
  90. package/src/orchestrate/prompt-builder.ts +155 -155
  91. package/src/orchestrate/ring-buffer.ts +37 -37
  92. package/src/orchestrate/task-graph.ts +389 -389
  93. package/src/project/aliases.ts +374 -374
  94. package/src/rules/adapters/trae.ts +56 -56
  95. package/src/rules/syncer.ts +235 -235
  96. package/src/search/query-expansion.ts +52 -52
  97. package/src/server/formation-timeout.ts +27 -27
  98. package/src/server.ts +14 -10
  99. package/src/skills/mini-skills.ts +386 -386
  100. package/src/store/chat-store.ts +119 -119
  101. package/src/store/graph-store.ts +249 -249
  102. package/src/store/mini-skill-store.ts +349 -349
  103. package/src/store/orama-store.ts +6 -4
  104. package/src/store/persistence-json.ts +212 -212
  105. package/src/store/persistence.ts +291 -291
  106. package/src/store/project-affinity.ts +195 -195
  107. package/src/team/event-bus.ts +76 -76
  108. package/src/team/handoff.ts +161 -161
  109. package/src/team/poll.ts +132 -132
  110. package/src/timeout.ts +67 -0
  111. package/src/types.ts +2 -0
  112. package/src/workspace/mcp-adapters/codex.ts +191 -191
  113. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  114. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  115. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  116. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  117. package/src/workspace/mcp-adapters/trae.ts +134 -134
  118. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  119. package/src/workspace/sanitizer.ts +60 -60
@@ -2141,6 +2141,7 @@ var init_resolved_config = __esm({
2141
2141
  // src/config.ts
2142
2142
  var config_exports = {};
2143
2143
  __export(config_exports, {
2144
+ defaultEmbeddingModelFor: () => defaultEmbeddingModelFor,
2144
2145
  getAgentLLMApiKey: () => getAgentLLMApiKey,
2145
2146
  getAgentLLMBaseUrl: () => getAgentLLMBaseUrl,
2146
2147
  getAgentLLMModel: () => getAgentLLMModel,
@@ -2203,7 +2204,19 @@ function getEmbeddingBaseUrl() {
2203
2204
  return getResolvedEmbeddingLane().baseUrl || "https://api.openai.com/v1";
2204
2205
  }
2205
2206
  function getEmbeddingModel() {
2206
- return getResolvedEmbeddingLane().model || "text-embedding-3-small";
2207
+ const lane = getResolvedEmbeddingLane();
2208
+ return defaultEmbeddingModelFor(lane.baseUrl, lane.model);
2209
+ }
2210
+ function defaultEmbeddingModelFor(baseUrl, explicitModel) {
2211
+ if (explicitModel) return explicitModel;
2212
+ try {
2213
+ const url = baseUrl ? new URL(baseUrl) : null;
2214
+ if (url && (url.hostname === "openrouter.ai" || url.hostname.endsWith(".openrouter.ai"))) {
2215
+ return "qwen/qwen3-embedding-8b";
2216
+ }
2217
+ } catch {
2218
+ }
2219
+ return "text-embedding-3-small";
2207
2220
  }
2208
2221
  function getEmbeddingDimensions() {
2209
2222
  return getResolvedEmbeddingLane().dimensions ?? null;
@@ -2742,6 +2755,8 @@ function getPreferredBatchSize(config) {
2742
2755
  }
2743
2756
  function parseBatchLimit(error) {
2744
2757
  if (!(error instanceof Error)) return null;
2758
+ const status = error instanceof EmbeddingAPIError ? error.status : Number(error.message.match(/Embedding API error \((\d{3})\)/i)?.[1] ?? 0);
2759
+ if (status > 0 && status !== 400) return null;
2745
2760
  const explicit = error.message.match(/should not be larger than\s+(\d+)/i);
2746
2761
  if (explicit) return parseInt(explicit[1], 10);
2747
2762
  if (/batch size/i.test(error.message)) {
@@ -2754,37 +2769,99 @@ async function fetchWithRetry(url, apiKey, body, options = {}, attempt = 0, nati
2754
2769
  const controller = new AbortController();
2755
2770
  const timeoutMs = typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs) ? Math.max(100, Math.min(Math.floor(options.timeoutMs), 6e4)) : 1e4;
2756
2771
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
2772
+ const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
2757
2773
  let response;
2758
2774
  try {
2759
2775
  response = await fetch(url, {
2760
2776
  method: "POST",
2761
2777
  headers: nativeGemini ? { "Content-Type": "application/json", "x-goog-api-key": apiKey } : { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
2762
2778
  body: JSON.stringify(body),
2763
- signal: controller.signal
2779
+ signal
2764
2780
  });
2765
2781
  } catch (err) {
2766
2782
  clearTimeout(timeout);
2767
- if (err instanceof Error && err.name === "AbortError") {
2783
+ if (err instanceof Error && (err.name === "AbortError" || controller.signal.aborted || options.signal?.aborted)) {
2768
2784
  throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
2769
2785
  }
2770
2786
  throw err;
2771
2787
  }
2772
- clearTimeout(timeout);
2773
- if (response.ok) {
2774
- return response.json();
2788
+ try {
2789
+ if (response.ok) {
2790
+ return await raceWithAbort(
2791
+ response.json(),
2792
+ signal
2793
+ );
2794
+ }
2795
+ if (options.retry !== false && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES2) {
2796
+ const delay2 = BASE_DELAY_MS * Math.pow(2, attempt);
2797
+ const retryAfter = response.headers.get("retry-after");
2798
+ const parsedRetryAfter = retryAfter ? parseInt(retryAfter, 10) * 1e3 : NaN;
2799
+ const waitMs = Number.isFinite(parsedRetryAfter) ? parsedRetryAfter : delay2;
2800
+ console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES2} in ${waitMs}ms`);
2801
+ await waitForRetry(waitMs, options.signal);
2802
+ return fetchWithRetry(url, apiKey, body, options, attempt + 1, nativeGemini);
2803
+ }
2804
+ const errorText2 = await raceWithAbort(
2805
+ response.text().catch(() => "unknown error"),
2806
+ signal
2807
+ );
2808
+ throw new EmbeddingAPIError(response.status, errorText2);
2809
+ } catch (err) {
2810
+ if (err instanceof Error && (err.name === "AbortError" || controller.signal.aborted || options.signal?.aborted)) {
2811
+ throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
2812
+ }
2813
+ throw err;
2814
+ } finally {
2815
+ clearTimeout(timeout);
2775
2816
  }
2776
- if (options.retry !== false && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES2) {
2777
- const delay2 = BASE_DELAY_MS * Math.pow(2, attempt);
2778
- const retryAfter = response.headers.get("retry-after");
2779
- const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : delay2;
2780
- console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES2} in ${waitMs}ms`);
2817
+ }
2818
+ function abortError() {
2819
+ const error = new Error("Embedding API request aborted");
2820
+ error.name = "AbortError";
2821
+ return error;
2822
+ }
2823
+ function raceWithAbort(promise, signal) {
2824
+ if (signal.aborted) return Promise.reject(abortError());
2825
+ return new Promise((resolve, reject) => {
2826
+ const onAbort = () => {
2827
+ signal.removeEventListener("abort", onAbort);
2828
+ reject(abortError());
2829
+ };
2830
+ signal.addEventListener("abort", onAbort, { once: true });
2831
+ promise.then(
2832
+ (value) => {
2833
+ signal.removeEventListener("abort", onAbort);
2834
+ resolve(value);
2835
+ },
2836
+ (error) => {
2837
+ signal.removeEventListener("abort", onAbort);
2838
+ reject(error);
2839
+ }
2840
+ );
2841
+ });
2842
+ }
2843
+ async function waitForRetry(waitMs, signal) {
2844
+ if (!signal) {
2781
2845
  await new Promise((resolve) => setTimeout(resolve, waitMs));
2782
- return fetchWithRetry(url, apiKey, body, options, attempt + 1, nativeGemini);
2846
+ return;
2783
2847
  }
2784
- const errorText2 = await response.text().catch(() => "unknown error");
2785
- throw new Error(`Embedding API error (${response.status}): ${errorText2}`);
2848
+ const abortSignal = signal;
2849
+ if (abortSignal.aborted) throw new Error("Embedding API retry aborted");
2850
+ await new Promise((resolve, reject) => {
2851
+ const timer = setTimeout(done, waitMs);
2852
+ const onAbort = () => {
2853
+ clearTimeout(timer);
2854
+ abortSignal.removeEventListener("abort", onAbort);
2855
+ reject(new Error("Embedding API retry aborted"));
2856
+ };
2857
+ function done() {
2858
+ abortSignal.removeEventListener("abort", onAbort);
2859
+ resolve();
2860
+ }
2861
+ abortSignal.addEventListener("abort", onAbort, { once: true });
2862
+ });
2786
2863
  }
2787
- var cache3, MAX_CACHE_SIZE3, diskCacheDirty2, diskSaveTimer, diskCacheLoaded, diskCacheLoadPromise, MAX_INPUT_CHARS, MAX_CONCURRENCY, DISK_SAVE_DEBOUNCE_MS, DEFAULT_MAX_BATCH_SIZE, DASHSCOPE_MAX_BATCH_SIZE, MAX_RETRIES2, BASE_DELAY_MS, APIEmbeddingProvider;
2864
+ var cache3, MAX_CACHE_SIZE3, diskCacheDirty2, diskSaveTimer, diskCacheLoaded, diskCacheLoadPromise, MAX_INPUT_CHARS, MAX_CONCURRENCY, DISK_SAVE_DEBOUNCE_MS, DEFAULT_MAX_BATCH_SIZE, DASHSCOPE_MAX_BATCH_SIZE, MAX_RETRIES2, BASE_DELAY_MS, EmbeddingAPIError, APIEmbeddingProvider;
2788
2865
  var init_api_provider = __esm({
2789
2866
  "src/embedding/api-provider.ts"() {
2790
2867
  "use strict";
@@ -2803,6 +2880,14 @@ var init_api_provider = __esm({
2803
2880
  DASHSCOPE_MAX_BATCH_SIZE = 10;
2804
2881
  MAX_RETRIES2 = 3;
2805
2882
  BASE_DELAY_MS = 500;
2883
+ EmbeddingAPIError = class extends Error {
2884
+ status;
2885
+ constructor(status, detail) {
2886
+ super(`Embedding API error (${status}): ${detail}`);
2887
+ this.name = "EmbeddingAPIError";
2888
+ this.status = status;
2889
+ }
2890
+ };
2806
2891
  APIEmbeddingProvider = class _APIEmbeddingProvider {
2807
2892
  name;
2808
2893
  dimensions;
@@ -3014,15 +3099,14 @@ var init_api_provider = __esm({
3014
3099
  }
3015
3100
  } catch (error) {
3016
3101
  const providerLimit = parseBatchLimit(error);
3017
- const fallbackSize = providerLimit ?? Math.ceil(chunkTexts.length / 2);
3018
- if (chunkTexts.length > 1 && fallbackSize < chunkTexts.length) {
3102
+ if (providerLimit !== null && chunkTexts.length > 1 && providerLimit < chunkTexts.length) {
3019
3103
  console.error(
3020
- `[memorix] Embedding batch too large for provider, retrying in chunks of ${fallbackSize}`
3104
+ `[memorix] Embedding batch too large for provider, retrying in chunks of ${providerLimit}`
3021
3105
  );
3022
- for (let start = 0; start < chunkTexts.length; start += fallbackSize) {
3106
+ for (let start = 0; start < chunkTexts.length; start += providerLimit) {
3023
3107
  await processChunk(
3024
- chunkTexts.slice(start, start + fallbackSize),
3025
- chunkIndices.slice(start, start + fallbackSize)
3108
+ chunkTexts.slice(start, start + providerLimit),
3109
+ chunkIndices.slice(start, start + providerLimit)
3026
3110
  );
3027
3111
  }
3028
3112
  return;
@@ -3886,22 +3970,23 @@ function getLLMConfig() {
3886
3970
  function setLLMConfig(config) {
3887
3971
  currentConfig = config;
3888
3972
  }
3889
- async function callLLM(systemPrompt, userMessage) {
3973
+ async function callLLM(systemPrompt, userMessage, signal) {
3890
3974
  if (!currentConfig) {
3891
3975
  throw new Error("LLM not configured. Set MEMORIX_LLM_API_KEY or OPENAI_API_KEY.");
3892
3976
  }
3893
3977
  if (currentConfig.provider === "anthropic") {
3894
- return callAnthropic(systemPrompt, userMessage);
3978
+ return callAnthropic(systemPrompt, userMessage, signal);
3895
3979
  }
3896
- return callOpenAICompatible(systemPrompt, userMessage);
3980
+ return callOpenAICompatible(systemPrompt, userMessage, signal);
3897
3981
  }
3898
- async function callOpenAICompatible(systemPrompt, userMessage) {
3982
+ async function callOpenAICompatible(systemPrompt, userMessage, signal) {
3899
3983
  const config = currentConfig;
3900
3984
  let base = config.baseUrl.replace(/\/+$/, "");
3901
3985
  if (!base.endsWith("/v1")) base += "/v1";
3902
3986
  const url = `${base}/chat/completions`;
3987
+ const fetchSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(LLM_CALL_TIMEOUT_MS)]) : AbortSignal.timeout(LLM_CALL_TIMEOUT_MS);
3903
3988
  const response = await fetch(url, {
3904
- signal: AbortSignal.timeout(LLM_CALL_TIMEOUT_MS),
3989
+ signal: fetchSignal,
3905
3990
  method: "POST",
3906
3991
  headers: {
3907
3992
  "Content-Type": "application/json",
@@ -3918,10 +4003,10 @@ async function callOpenAICompatible(systemPrompt, userMessage) {
3918
4003
  })
3919
4004
  });
3920
4005
  if (!response.ok) {
3921
- const error = await readResponseText(response, void 0, 64 * 1024).catch(() => "unknown error");
4006
+ const error = await readResponseText(response, signal, 64 * 1024).catch(() => "unknown error");
3922
4007
  throw new Error(`LLM API error (${response.status}): ${error}`);
3923
4008
  }
3924
- const data = await readResponseJson(response);
4009
+ const data = await readResponseJson(response, signal);
3925
4010
  return {
3926
4011
  content: data.choices[0]?.message?.content ?? "",
3927
4012
  usage: data.usage ? {
@@ -3930,11 +4015,12 @@ async function callOpenAICompatible(systemPrompt, userMessage) {
3930
4015
  } : void 0
3931
4016
  };
3932
4017
  }
3933
- async function callAnthropic(systemPrompt, userMessage) {
4018
+ async function callAnthropic(systemPrompt, userMessage, signal) {
3934
4019
  const config = currentConfig;
3935
4020
  const url = `${config.baseUrl}/messages`;
4021
+ const fetchSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(LLM_CALL_TIMEOUT_MS)]) : AbortSignal.timeout(LLM_CALL_TIMEOUT_MS);
3936
4022
  const response = await fetch(url, {
3937
- signal: AbortSignal.timeout(LLM_CALL_TIMEOUT_MS),
4023
+ signal: fetchSignal,
3938
4024
  method: "POST",
3939
4025
  headers: {
3940
4026
  "Content-Type": "application/json",
@@ -3952,10 +4038,10 @@ async function callAnthropic(systemPrompt, userMessage) {
3952
4038
  })
3953
4039
  });
3954
4040
  if (!response.ok) {
3955
- const error = await readResponseText(response, void 0, 64 * 1024).catch(() => "unknown error");
4041
+ const error = await readResponseText(response, signal, 64 * 1024).catch(() => "unknown error");
3956
4042
  throw new Error(`Anthropic API error (${response.status}): ${error}`);
3957
4043
  }
3958
- const data = await readResponseJson(response);
4044
+ const data = await readResponseJson(response, signal);
3959
4045
  return {
3960
4046
  content: data.content[0]?.text ?? "",
3961
4047
  usage: data.usage ? {
@@ -4460,6 +4546,57 @@ function withTimeout(promise, ms, label) {
4460
4546
  );
4461
4547
  });
4462
4548
  }
4549
+ function withTimeoutSignal(operation, ms, label, parentSignal) {
4550
+ const controller = new AbortController();
4551
+ const signal = parentSignal ? AbortSignal.any([parentSignal, controller.signal]) : controller.signal;
4552
+ return new Promise((resolve, reject) => {
4553
+ let settled = false;
4554
+ let timedOut = false;
4555
+ let timer;
4556
+ const cleanup = () => {
4557
+ if (timer !== void 0) clearTimeout(timer);
4558
+ parentSignal?.removeEventListener("abort", onParentAbort);
4559
+ };
4560
+ const onParentAbort = () => {
4561
+ if (settled || timedOut) return;
4562
+ settled = true;
4563
+ cleanup();
4564
+ reject(parentSignal?.reason ?? new Error(`${label} aborted`));
4565
+ };
4566
+ parentSignal?.addEventListener("abort", onParentAbort, { once: true });
4567
+ timer = setTimeout(() => {
4568
+ if (settled) return;
4569
+ timedOut = true;
4570
+ controller.abort(new Error(`${label} timed out after ${ms}ms`));
4571
+ settled = true;
4572
+ cleanup();
4573
+ reject(new Error(`${label} timed out after ${ms}ms`));
4574
+ }, ms);
4575
+ let pending;
4576
+ try {
4577
+ pending = operation(signal);
4578
+ } catch (error) {
4579
+ settled = true;
4580
+ cleanup();
4581
+ reject(error);
4582
+ return;
4583
+ }
4584
+ pending.then(
4585
+ (value) => {
4586
+ if (settled) return;
4587
+ settled = true;
4588
+ cleanup();
4589
+ resolve(value);
4590
+ },
4591
+ (error) => {
4592
+ if (settled) return;
4593
+ settled = true;
4594
+ cleanup();
4595
+ reject(timedOut ? new Error(`${label} timed out after ${ms}ms`) : error);
4596
+ }
4597
+ );
4598
+ });
4599
+ }
4463
4600
  var init_timeout = __esm({
4464
4601
  "src/timeout.ts"() {
4465
4602
  "use strict";
@@ -5324,16 +5461,18 @@ async function searchObservations(options) {
5324
5461
  );
5325
5462
  } else {
5326
5463
  const EMBEDDING_TIMEOUT_MS = 15e3;
5327
- queryVector = await withTimeout(
5328
- provider2.embedInput ? provider2.embedInput(
5464
+ queryVector = await withTimeoutSignal(
5465
+ (signal) => provider2.embedInput ? provider2.embedInput(
5329
5466
  { modality: "text", text: expandedEmbeddingQuery },
5330
- { intent: "query", timeoutMs: EMBEDDING_TIMEOUT_MS, retry: false }
5467
+ { intent: "query", timeoutMs: EMBEDDING_TIMEOUT_MS, retry: false, signal }
5331
5468
  ) : provider2.embed(expandedEmbeddingQuery, {
5332
5469
  timeoutMs: EMBEDDING_TIMEOUT_MS,
5333
- retry: false
5470
+ retry: false,
5471
+ signal
5334
5472
  }),
5335
5473
  EMBEDDING_TIMEOUT_MS,
5336
- "Embedding"
5474
+ "Embedding",
5475
+ options.signal
5337
5476
  );
5338
5477
  mark("embedding");
5339
5478
  const cjkRatio = (originalQuery.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g) || []).length / originalQuery.length;
@@ -143253,7 +143392,13 @@ async function maintainLongTermMemories(input) {
143253
143392
  }
143254
143393
  for (const group of groups.values()) {
143255
143394
  if (group.length < 2) continue;
143256
- group.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
143395
+ group.sort((a, b) => {
143396
+ const byCreatedAt = Date.parse(b.createdAt) - Date.parse(a.createdAt);
143397
+ if (byCreatedAt !== 0) return byCreatedAt;
143398
+ const byUpdatedAt = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
143399
+ if (byUpdatedAt !== 0) return byUpdatedAt;
143400
+ return b.id.localeCompare(a.id);
143401
+ });
143257
143402
  const newest = group[0];
143258
143403
  for (const older of group.slice(1)) {
143259
143404
  try {
@@ -2,6 +2,44 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [Unreleased]
6
+
7
+ ## [1.5.1] - 2026-08-16
8
+
9
+ ### Fixed
10
+ - **Bounded formation cancellation** -- Formation deadlines now abort the
11
+ LLM and vector-search work they started, including response-body reads and
12
+ retry waits. A provider that ignores cancellation still cannot hold the
13
+ caller past its deadline.
14
+ - **Embedding error classification** -- Only an explicit HTTP 400 batch-shape
15
+ error may trigger batch splitting. Authentication, billing, quota, and
16
+ upstream errors fail once instead of multiplying latency through recursive
17
+ retries.
18
+
19
+ ## [1.5.0] - 2026-08-14
20
+
21
+ ### Added
22
+ - **Stability stress suite** -- Five deterministic, offline stress exams now
23
+ run in the normal test suite: a 2,000-observation corpus (planted needles,
24
+ ranked determinism, project isolation), 60-way parallel writes plus
25
+ 20-way same-topic upserts, session churn across projects with parallel
26
+ starts, 300-record long-term maintenance exactness, and repeated CLI
27
+ invocations. Live embedding stress exams are env-gated
28
+ (`MEMORIX_RUN_LIVE_EMBEDDING_TESTS=1`) and never run in CI.
29
+ - **Provider-aware embedding default** -- When the embedding base URL
30
+ points at OpenRouter and no model is configured, the default is now
31
+ `qwen/qwen3-embedding-8b` (4096 dimensions) instead of the OpenAI-only
32
+ default. An explicitly configured model always wins.
33
+
34
+ ### Changed
35
+ - **Verified OpenRouter embedding lane** -- The `api` embedding provider
36
+ was exercised live against `qwen/qwen3-embedding-8b` on OpenRouter:
37
+ a 100-text batch, cache round-trip, and provider-singleton stability all
38
+ pass. Docs cover the env-var form and borrowing `OPENROUTER_API_KEY`.
39
+ - **Multimodal vision option documented** -- Image analysis can run on the
40
+ LLM lane through an OpenAI-compatible vision endpoint, including
41
+ OpenRouter models such as `qwen/qwen3-vl-8b-instruct`.
42
+
5
43
  ## [1.4.6] - 2026-08-14
6
44
 
7
45
  ### Added