memorix 1.5.0 → 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 (117) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/TEAM.md +86 -86
  3. package/dist/cli/index.js +211 -69
  4. package/dist/cli/memcode.js +0 -0
  5. package/dist/index.js +211 -69
  6. package/dist/maintenance-runner.js +169 -37
  7. package/dist/media-audio-runner.js +169 -37
  8. package/dist/media-video-runner.js +169 -37
  9. package/dist/memcode-runtime/CHANGELOG.md +14 -0
  10. package/dist/sdk.js +210 -68
  11. package/dist/types.d.ts +2 -0
  12. package/dist/vector-backfill-runner.js +169 -37
  13. package/docs/DESIGN_DECISIONS.md +357 -357
  14. package/package.json +1 -1
  15. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  16. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  17. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  18. package/plugins/copilot/memorix/plugin.json +1 -1
  19. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  20. package/plugins/omp/memorix/package.json +1 -1
  21. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  22. package/plugins/pi/memorix/package.json +1 -1
  23. package/server.json +2 -2
  24. package/src/audit/index.ts +156 -156
  25. package/src/cli/commands/formation.ts +48 -48
  26. package/src/cli/commands/git-hook-install.ts +111 -111
  27. package/src/cli/commands/handoff.ts +54 -54
  28. package/src/cli/commands/hooks-status.ts +63 -63
  29. package/src/cli/commands/ingest.ts +44 -44
  30. package/src/cli/commands/integrate-shared.ts +15 -15
  31. package/src/cli/commands/lock.ts +82 -82
  32. package/src/cli/commands/message.ts +104 -104
  33. package/src/cli/commands/poll.ts +58 -58
  34. package/src/cli/commands/reasoning.ts +118 -118
  35. package/src/cli/commands/skills.ts +114 -114
  36. package/src/cli/commands/task.ts +167 -167
  37. package/src/cli/commands/transfer.ts +47 -47
  38. package/src/cli/index.ts +406 -406
  39. package/src/cli/tui/ChatView.tsx +234 -234
  40. package/src/cli/tui/CommandBar.tsx +312 -312
  41. package/src/cli/tui/ContextRail.tsx +118 -118
  42. package/src/cli/tui/HeaderBar.tsx +72 -72
  43. package/src/cli/tui/LogoBanner.tsx +51 -51
  44. package/src/cli/tui/index.ts +41 -41
  45. package/src/cli/version.ts +7 -7
  46. package/src/cli/workbench.ts +1 -1
  47. package/src/dashboard/project-classification.ts +64 -64
  48. package/src/embedding/api-provider.ts +105 -22
  49. package/src/embedding/provider.ts +2 -0
  50. package/src/git/hooks-path.ts +85 -85
  51. package/src/hooks/pattern-detector.ts +173 -173
  52. package/src/hooks/significance-filter.ts +250 -250
  53. package/src/llm/memory-manager.ts +328 -328
  54. package/src/llm/provider.ts +17 -8
  55. package/src/llm/quality.ts +248 -248
  56. package/src/memory/attribution-guard.ts +249 -249
  57. package/src/memory/disclosure-policy.ts +135 -135
  58. package/src/memory/entity-extractor.ts +197 -197
  59. package/src/memory/formation/evaluate.ts +217 -217
  60. package/src/memory/formation/extract.ts +6 -3
  61. package/src/memory/formation/index.ts +2 -1
  62. package/src/memory/formation/resolve.ts +10 -6
  63. package/src/memory/formation/types.ts +3 -1
  64. package/src/memory/freshness.ts +122 -122
  65. package/src/memory/graph.ts +197 -197
  66. package/src/memory/long-term.ts +7 -1
  67. package/src/memory/refs.ts +94 -94
  68. package/src/memory/secret-filter.ts +79 -79
  69. package/src/orchestrate/adapters/claude-stream.ts +192 -192
  70. package/src/orchestrate/adapters/claude.ts +111 -111
  71. package/src/orchestrate/adapters/codex-stream.ts +134 -134
  72. package/src/orchestrate/adapters/codex.ts +41 -41
  73. package/src/orchestrate/adapters/gemini-stream.ts +166 -166
  74. package/src/orchestrate/adapters/gemini.ts +42 -42
  75. package/src/orchestrate/adapters/index.ts +73 -73
  76. package/src/orchestrate/adapters/opencode-stream.ts +143 -143
  77. package/src/orchestrate/adapters/opencode.ts +47 -47
  78. package/src/orchestrate/adapters/types.ts +77 -77
  79. package/src/orchestrate/capability-router.ts +284 -284
  80. package/src/orchestrate/cost-tracker.ts +219 -219
  81. package/src/orchestrate/error-recovery.ts +191 -191
  82. package/src/orchestrate/evidence.ts +140 -140
  83. package/src/orchestrate/ledger.ts +110 -110
  84. package/src/orchestrate/memorix-bridge.ts +343 -343
  85. package/src/orchestrate/output-budget.ts +80 -80
  86. package/src/orchestrate/permission.ts +152 -152
  87. package/src/orchestrate/pipeline-trace.ts +131 -131
  88. package/src/orchestrate/prompt-builder.ts +155 -155
  89. package/src/orchestrate/ring-buffer.ts +37 -37
  90. package/src/orchestrate/task-graph.ts +389 -389
  91. package/src/project/aliases.ts +374 -374
  92. package/src/rules/adapters/trae.ts +56 -56
  93. package/src/rules/syncer.ts +235 -235
  94. package/src/search/query-expansion.ts +52 -52
  95. package/src/server/formation-timeout.ts +27 -27
  96. package/src/server.ts +14 -10
  97. package/src/skills/mini-skills.ts +386 -386
  98. package/src/store/chat-store.ts +119 -119
  99. package/src/store/graph-store.ts +249 -249
  100. package/src/store/mini-skill-store.ts +349 -349
  101. package/src/store/orama-store.ts +6 -4
  102. package/src/store/persistence-json.ts +212 -212
  103. package/src/store/persistence.ts +291 -291
  104. package/src/store/project-affinity.ts +195 -195
  105. package/src/team/event-bus.ts +76 -76
  106. package/src/team/handoff.ts +161 -161
  107. package/src/team/poll.ts +132 -132
  108. package/src/timeout.ts +67 -0
  109. package/src/types.ts +2 -0
  110. package/src/workspace/mcp-adapters/codex.ts +191 -191
  111. package/src/workspace/mcp-adapters/copilot.ts +105 -105
  112. package/src/workspace/mcp-adapters/cursor.ts +53 -53
  113. package/src/workspace/mcp-adapters/kiro.ts +64 -64
  114. package/src/workspace/mcp-adapters/opencode.ts +123 -123
  115. package/src/workspace/mcp-adapters/trae.ts +134 -134
  116. package/src/workspace/mcp-adapters/windsurf.ts +91 -91
  117. package/src/workspace/sanitizer.ts +60 -60
@@ -2083,22 +2083,23 @@ function getLLMConfig() {
2083
2083
  function setLLMConfig(config) {
2084
2084
  currentConfig = config;
2085
2085
  }
2086
- async function callLLM(systemPrompt, userMessage) {
2086
+ async function callLLM(systemPrompt, userMessage, signal) {
2087
2087
  if (!currentConfig) {
2088
2088
  throw new Error("LLM not configured. Set MEMORIX_LLM_API_KEY or OPENAI_API_KEY.");
2089
2089
  }
2090
2090
  if (currentConfig.provider === "anthropic") {
2091
- return callAnthropic(systemPrompt, userMessage);
2091
+ return callAnthropic(systemPrompt, userMessage, signal);
2092
2092
  }
2093
- return callOpenAICompatible(systemPrompt, userMessage);
2093
+ return callOpenAICompatible(systemPrompt, userMessage, signal);
2094
2094
  }
2095
- async function callOpenAICompatible(systemPrompt, userMessage) {
2095
+ async function callOpenAICompatible(systemPrompt, userMessage, signal) {
2096
2096
  const config = currentConfig;
2097
2097
  let base = config.baseUrl.replace(/\/+$/, "");
2098
2098
  if (!base.endsWith("/v1")) base += "/v1";
2099
2099
  const url = `${base}/chat/completions`;
2100
+ const fetchSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(LLM_CALL_TIMEOUT_MS)]) : AbortSignal.timeout(LLM_CALL_TIMEOUT_MS);
2100
2101
  const response = await fetch(url, {
2101
- signal: AbortSignal.timeout(LLM_CALL_TIMEOUT_MS),
2102
+ signal: fetchSignal,
2102
2103
  method: "POST",
2103
2104
  headers: {
2104
2105
  "Content-Type": "application/json",
@@ -2115,10 +2116,10 @@ async function callOpenAICompatible(systemPrompt, userMessage) {
2115
2116
  })
2116
2117
  });
2117
2118
  if (!response.ok) {
2118
- const error = await readResponseText(response, void 0, 64 * 1024).catch(() => "unknown error");
2119
+ const error = await readResponseText(response, signal, 64 * 1024).catch(() => "unknown error");
2119
2120
  throw new Error(`LLM API error (${response.status}): ${error}`);
2120
2121
  }
2121
- const data = await readResponseJson(response);
2122
+ const data = await readResponseJson(response, signal);
2122
2123
  return {
2123
2124
  content: data.choices[0]?.message?.content ?? "",
2124
2125
  usage: data.usage ? {
@@ -2127,11 +2128,12 @@ async function callOpenAICompatible(systemPrompt, userMessage) {
2127
2128
  } : void 0
2128
2129
  };
2129
2130
  }
2130
- async function callAnthropic(systemPrompt, userMessage) {
2131
+ async function callAnthropic(systemPrompt, userMessage, signal) {
2131
2132
  const config = currentConfig;
2132
2133
  const url = `${config.baseUrl}/messages`;
2134
+ const fetchSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(LLM_CALL_TIMEOUT_MS)]) : AbortSignal.timeout(LLM_CALL_TIMEOUT_MS);
2133
2135
  const response = await fetch(url, {
2134
- signal: AbortSignal.timeout(LLM_CALL_TIMEOUT_MS),
2136
+ signal: fetchSignal,
2135
2137
  method: "POST",
2136
2138
  headers: {
2137
2139
  "Content-Type": "application/json",
@@ -2149,10 +2151,10 @@ async function callAnthropic(systemPrompt, userMessage) {
2149
2151
  })
2150
2152
  });
2151
2153
  if (!response.ok) {
2152
- const error = await readResponseText(response, void 0, 64 * 1024).catch(() => "unknown error");
2154
+ const error = await readResponseText(response, signal, 64 * 1024).catch(() => "unknown error");
2153
2155
  throw new Error(`Anthropic API error (${response.status}): ${error}`);
2154
2156
  }
2155
- const data = await readResponseJson(response);
2157
+ const data = await readResponseJson(response, signal);
2156
2158
  return {
2157
2159
  content: data.content[0]?.text ?? "",
2158
2160
  usage: data.usage ? {
@@ -3855,6 +3857,8 @@ function getPreferredBatchSize(config) {
3855
3857
  }
3856
3858
  function parseBatchLimit(error) {
3857
3859
  if (!(error instanceof Error)) return null;
3860
+ const status = error instanceof EmbeddingAPIError ? error.status : Number(error.message.match(/Embedding API error \((\d{3})\)/i)?.[1] ?? 0);
3861
+ if (status > 0 && status !== 400) return null;
3858
3862
  const explicit = error.message.match(/should not be larger than\s+(\d+)/i);
3859
3863
  if (explicit) return parseInt(explicit[1], 10);
3860
3864
  if (/batch size/i.test(error.message)) {
@@ -3867,37 +3871,99 @@ async function fetchWithRetry(url, apiKey, body, options = {}, attempt = 0, nati
3867
3871
  const controller = new AbortController();
3868
3872
  const timeoutMs = typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs) ? Math.max(100, Math.min(Math.floor(options.timeoutMs), 6e4)) : 1e4;
3869
3873
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
3874
+ const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
3870
3875
  let response;
3871
3876
  try {
3872
3877
  response = await fetch(url, {
3873
3878
  method: "POST",
3874
3879
  headers: nativeGemini ? { "Content-Type": "application/json", "x-goog-api-key": apiKey } : { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
3875
3880
  body: JSON.stringify(body),
3876
- signal: controller.signal
3881
+ signal
3877
3882
  });
3878
3883
  } catch (err) {
3879
3884
  clearTimeout(timeout);
3880
- if (err instanceof Error && err.name === "AbortError") {
3885
+ if (err instanceof Error && (err.name === "AbortError" || controller.signal.aborted || options.signal?.aborted)) {
3881
3886
  throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
3882
3887
  }
3883
3888
  throw err;
3884
3889
  }
3885
- clearTimeout(timeout);
3886
- if (response.ok) {
3887
- return response.json();
3890
+ try {
3891
+ if (response.ok) {
3892
+ return await raceWithAbort(
3893
+ response.json(),
3894
+ signal
3895
+ );
3896
+ }
3897
+ if (options.retry !== false && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES2) {
3898
+ const delay2 = BASE_DELAY_MS * Math.pow(2, attempt);
3899
+ const retryAfter = response.headers.get("retry-after");
3900
+ const parsedRetryAfter = retryAfter ? parseInt(retryAfter, 10) * 1e3 : NaN;
3901
+ const waitMs = Number.isFinite(parsedRetryAfter) ? parsedRetryAfter : delay2;
3902
+ console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES2} in ${waitMs}ms`);
3903
+ await waitForRetry(waitMs, options.signal);
3904
+ return fetchWithRetry(url, apiKey, body, options, attempt + 1, nativeGemini);
3905
+ }
3906
+ const errorText2 = await raceWithAbort(
3907
+ response.text().catch(() => "unknown error"),
3908
+ signal
3909
+ );
3910
+ throw new EmbeddingAPIError(response.status, errorText2);
3911
+ } catch (err) {
3912
+ if (err instanceof Error && (err.name === "AbortError" || controller.signal.aborted || options.signal?.aborted)) {
3913
+ throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
3914
+ }
3915
+ throw err;
3916
+ } finally {
3917
+ clearTimeout(timeout);
3888
3918
  }
3889
- if (options.retry !== false && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES2) {
3890
- const delay2 = BASE_DELAY_MS * Math.pow(2, attempt);
3891
- const retryAfter = response.headers.get("retry-after");
3892
- const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : delay2;
3893
- console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES2} in ${waitMs}ms`);
3919
+ }
3920
+ function abortError() {
3921
+ const error = new Error("Embedding API request aborted");
3922
+ error.name = "AbortError";
3923
+ return error;
3924
+ }
3925
+ function raceWithAbort(promise, signal) {
3926
+ if (signal.aborted) return Promise.reject(abortError());
3927
+ return new Promise((resolve, reject) => {
3928
+ const onAbort = () => {
3929
+ signal.removeEventListener("abort", onAbort);
3930
+ reject(abortError());
3931
+ };
3932
+ signal.addEventListener("abort", onAbort, { once: true });
3933
+ promise.then(
3934
+ (value) => {
3935
+ signal.removeEventListener("abort", onAbort);
3936
+ resolve(value);
3937
+ },
3938
+ (error) => {
3939
+ signal.removeEventListener("abort", onAbort);
3940
+ reject(error);
3941
+ }
3942
+ );
3943
+ });
3944
+ }
3945
+ async function waitForRetry(waitMs, signal) {
3946
+ if (!signal) {
3894
3947
  await new Promise((resolve) => setTimeout(resolve, waitMs));
3895
- return fetchWithRetry(url, apiKey, body, options, attempt + 1, nativeGemini);
3948
+ return;
3896
3949
  }
3897
- const errorText2 = await response.text().catch(() => "unknown error");
3898
- throw new Error(`Embedding API error (${response.status}): ${errorText2}`);
3950
+ const abortSignal = signal;
3951
+ if (abortSignal.aborted) throw new Error("Embedding API retry aborted");
3952
+ await new Promise((resolve, reject) => {
3953
+ const timer = setTimeout(done, waitMs);
3954
+ const onAbort = () => {
3955
+ clearTimeout(timer);
3956
+ abortSignal.removeEventListener("abort", onAbort);
3957
+ reject(new Error("Embedding API retry aborted"));
3958
+ };
3959
+ function done() {
3960
+ abortSignal.removeEventListener("abort", onAbort);
3961
+ resolve();
3962
+ }
3963
+ abortSignal.addEventListener("abort", onAbort, { once: true });
3964
+ });
3899
3965
  }
3900
- 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;
3966
+ 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;
3901
3967
  var init_api_provider = __esm({
3902
3968
  "src/embedding/api-provider.ts"() {
3903
3969
  "use strict";
@@ -3916,6 +3982,14 @@ var init_api_provider = __esm({
3916
3982
  DASHSCOPE_MAX_BATCH_SIZE = 10;
3917
3983
  MAX_RETRIES2 = 3;
3918
3984
  BASE_DELAY_MS = 500;
3985
+ EmbeddingAPIError = class extends Error {
3986
+ status;
3987
+ constructor(status, detail) {
3988
+ super(`Embedding API error (${status}): ${detail}`);
3989
+ this.name = "EmbeddingAPIError";
3990
+ this.status = status;
3991
+ }
3992
+ };
3919
3993
  APIEmbeddingProvider = class _APIEmbeddingProvider {
3920
3994
  name;
3921
3995
  dimensions;
@@ -4127,15 +4201,14 @@ var init_api_provider = __esm({
4127
4201
  }
4128
4202
  } catch (error) {
4129
4203
  const providerLimit = parseBatchLimit(error);
4130
- const fallbackSize = providerLimit ?? Math.ceil(chunkTexts.length / 2);
4131
- if (chunkTexts.length > 1 && fallbackSize < chunkTexts.length) {
4204
+ if (providerLimit !== null && chunkTexts.length > 1 && providerLimit < chunkTexts.length) {
4132
4205
  console.error(
4133
- `[memorix] Embedding batch too large for provider, retrying in chunks of ${fallbackSize}`
4206
+ `[memorix] Embedding batch too large for provider, retrying in chunks of ${providerLimit}`
4134
4207
  );
4135
- for (let start = 0; start < chunkTexts.length; start += fallbackSize) {
4208
+ for (let start = 0; start < chunkTexts.length; start += providerLimit) {
4136
4209
  await processChunk(
4137
- chunkTexts.slice(start, start + fallbackSize),
4138
- chunkIndices.slice(start, start + fallbackSize)
4210
+ chunkTexts.slice(start, start + providerLimit),
4211
+ chunkIndices.slice(start, start + providerLimit)
4139
4212
  );
4140
4213
  }
4141
4214
  return;
@@ -4959,6 +5032,57 @@ function withTimeout(promise, ms, label) {
4959
5032
  );
4960
5033
  });
4961
5034
  }
5035
+ function withTimeoutSignal(operation, ms, label, parentSignal) {
5036
+ const controller = new AbortController();
5037
+ const signal = parentSignal ? AbortSignal.any([parentSignal, controller.signal]) : controller.signal;
5038
+ return new Promise((resolve, reject) => {
5039
+ let settled = false;
5040
+ let timedOut = false;
5041
+ let timer;
5042
+ const cleanup = () => {
5043
+ if (timer !== void 0) clearTimeout(timer);
5044
+ parentSignal?.removeEventListener("abort", onParentAbort);
5045
+ };
5046
+ const onParentAbort = () => {
5047
+ if (settled || timedOut) return;
5048
+ settled = true;
5049
+ cleanup();
5050
+ reject(parentSignal?.reason ?? new Error(`${label} aborted`));
5051
+ };
5052
+ parentSignal?.addEventListener("abort", onParentAbort, { once: true });
5053
+ timer = setTimeout(() => {
5054
+ if (settled) return;
5055
+ timedOut = true;
5056
+ controller.abort(new Error(`${label} timed out after ${ms}ms`));
5057
+ settled = true;
5058
+ cleanup();
5059
+ reject(new Error(`${label} timed out after ${ms}ms`));
5060
+ }, ms);
5061
+ let pending;
5062
+ try {
5063
+ pending = operation(signal);
5064
+ } catch (error) {
5065
+ settled = true;
5066
+ cleanup();
5067
+ reject(error);
5068
+ return;
5069
+ }
5070
+ pending.then(
5071
+ (value) => {
5072
+ if (settled) return;
5073
+ settled = true;
5074
+ cleanup();
5075
+ resolve(value);
5076
+ },
5077
+ (error) => {
5078
+ if (settled) return;
5079
+ settled = true;
5080
+ cleanup();
5081
+ reject(timedOut ? new Error(`${label} timed out after ${ms}ms`) : error);
5082
+ }
5083
+ );
5084
+ });
5085
+ }
4962
5086
  var init_timeout = __esm({
4963
5087
  "src/timeout.ts"() {
4964
5088
  "use strict";
@@ -5823,16 +5947,18 @@ async function searchObservations(options) {
5823
5947
  );
5824
5948
  } else {
5825
5949
  const EMBEDDING_TIMEOUT_MS = 15e3;
5826
- queryVector = await withTimeout(
5827
- provider2.embedInput ? provider2.embedInput(
5950
+ queryVector = await withTimeoutSignal(
5951
+ (signal) => provider2.embedInput ? provider2.embedInput(
5828
5952
  { modality: "text", text: expandedEmbeddingQuery },
5829
- { intent: "query", timeoutMs: EMBEDDING_TIMEOUT_MS, retry: false }
5953
+ { intent: "query", timeoutMs: EMBEDDING_TIMEOUT_MS, retry: false, signal }
5830
5954
  ) : provider2.embed(expandedEmbeddingQuery, {
5831
5955
  timeoutMs: EMBEDDING_TIMEOUT_MS,
5832
- retry: false
5956
+ retry: false,
5957
+ signal
5833
5958
  }),
5834
5959
  EMBEDDING_TIMEOUT_MS,
5835
- "Embedding"
5960
+ "Embedding",
5961
+ options.signal
5836
5962
  );
5837
5963
  mark("embedding");
5838
5964
  const cjkRatio = (originalQuery.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g) || []).length / originalQuery.length;
@@ -143295,7 +143421,13 @@ async function maintainLongTermMemories(input) {
143295
143421
  }
143296
143422
  for (const group of groups.values()) {
143297
143423
  if (group.length < 2) continue;
143298
- group.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
143424
+ group.sort((a, b) => {
143425
+ const byCreatedAt = Date.parse(b.createdAt) - Date.parse(a.createdAt);
143426
+ if (byCreatedAt !== 0) return byCreatedAt;
143427
+ const byUpdatedAt = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
143428
+ if (byUpdatedAt !== 0) return byUpdatedAt;
143429
+ return b.id.localeCompare(a.id);
143430
+ });
143299
143431
  const newest = group[0];
143300
143432
  for (const older of group.slice(1)) {
143301
143433
  try {
@@ -3738,6 +3738,8 @@ function getPreferredBatchSize(config) {
3738
3738
  }
3739
3739
  function parseBatchLimit(error) {
3740
3740
  if (!(error instanceof Error)) return null;
3741
+ const status = error instanceof EmbeddingAPIError ? error.status : Number(error.message.match(/Embedding API error \((\d{3})\)/i)?.[1] ?? 0);
3742
+ if (status > 0 && status !== 400) return null;
3741
3743
  const explicit = error.message.match(/should not be larger than\s+(\d+)/i);
3742
3744
  if (explicit) return parseInt(explicit[1], 10);
3743
3745
  if (/batch size/i.test(error.message)) {
@@ -3750,37 +3752,99 @@ async function fetchWithRetry(url, apiKey, body, options = {}, attempt = 0, nati
3750
3752
  const controller = new AbortController();
3751
3753
  const timeoutMs = typeof options.timeoutMs === "number" && Number.isFinite(options.timeoutMs) ? Math.max(100, Math.min(Math.floor(options.timeoutMs), 6e4)) : 1e4;
3752
3754
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
3755
+ const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
3753
3756
  let response;
3754
3757
  try {
3755
3758
  response = await fetch(url, {
3756
3759
  method: "POST",
3757
3760
  headers: nativeGemini ? { "Content-Type": "application/json", "x-goog-api-key": apiKey } : { "Content-Type": "application/json", "Authorization": `Bearer ${apiKey}` },
3758
3761
  body: JSON.stringify(body),
3759
- signal: controller.signal
3762
+ signal
3760
3763
  });
3761
3764
  } catch (err) {
3762
3765
  clearTimeout(timeout);
3763
- if (err instanceof Error && err.name === "AbortError") {
3766
+ if (err instanceof Error && (err.name === "AbortError" || controller.signal.aborted || options.signal?.aborted)) {
3764
3767
  throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
3765
3768
  }
3766
3769
  throw err;
3767
3770
  }
3768
- clearTimeout(timeout);
3769
- if (response.ok) {
3770
- return response.json();
3771
+ try {
3772
+ if (response.ok) {
3773
+ return await raceWithAbort(
3774
+ response.json(),
3775
+ signal
3776
+ );
3777
+ }
3778
+ if (options.retry !== false && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES2) {
3779
+ const delay2 = BASE_DELAY_MS * Math.pow(2, attempt);
3780
+ const retryAfter = response.headers.get("retry-after");
3781
+ const parsedRetryAfter = retryAfter ? parseInt(retryAfter, 10) * 1e3 : NaN;
3782
+ const waitMs = Number.isFinite(parsedRetryAfter) ? parsedRetryAfter : delay2;
3783
+ console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES2} in ${waitMs}ms`);
3784
+ await waitForRetry(waitMs, options.signal);
3785
+ return fetchWithRetry(url, apiKey, body, options, attempt + 1, nativeGemini);
3786
+ }
3787
+ const errorText2 = await raceWithAbort(
3788
+ response.text().catch(() => "unknown error"),
3789
+ signal
3790
+ );
3791
+ throw new EmbeddingAPIError(response.status, errorText2);
3792
+ } catch (err) {
3793
+ if (err instanceof Error && (err.name === "AbortError" || controller.signal.aborted || options.signal?.aborted)) {
3794
+ throw new Error(`Embedding API timeout after ${timeoutMs}ms: ${url}`);
3795
+ }
3796
+ throw err;
3797
+ } finally {
3798
+ clearTimeout(timeout);
3771
3799
  }
3772
- if (options.retry !== false && (response.status === 429 || response.status >= 500) && attempt < MAX_RETRIES2) {
3773
- const delay2 = BASE_DELAY_MS * Math.pow(2, attempt);
3774
- const retryAfter = response.headers.get("retry-after");
3775
- const waitMs = retryAfter ? parseInt(retryAfter, 10) * 1e3 : delay2;
3776
- console.error(`[memorix] Embedding API ${response.status}, retry ${attempt + 1}/${MAX_RETRIES2} in ${waitMs}ms`);
3800
+ }
3801
+ function abortError() {
3802
+ const error = new Error("Embedding API request aborted");
3803
+ error.name = "AbortError";
3804
+ return error;
3805
+ }
3806
+ function raceWithAbort(promise, signal) {
3807
+ if (signal.aborted) return Promise.reject(abortError());
3808
+ return new Promise((resolve, reject) => {
3809
+ const onAbort = () => {
3810
+ signal.removeEventListener("abort", onAbort);
3811
+ reject(abortError());
3812
+ };
3813
+ signal.addEventListener("abort", onAbort, { once: true });
3814
+ promise.then(
3815
+ (value) => {
3816
+ signal.removeEventListener("abort", onAbort);
3817
+ resolve(value);
3818
+ },
3819
+ (error) => {
3820
+ signal.removeEventListener("abort", onAbort);
3821
+ reject(error);
3822
+ }
3823
+ );
3824
+ });
3825
+ }
3826
+ async function waitForRetry(waitMs, signal) {
3827
+ if (!signal) {
3777
3828
  await new Promise((resolve) => setTimeout(resolve, waitMs));
3778
- return fetchWithRetry(url, apiKey, body, options, attempt + 1, nativeGemini);
3829
+ return;
3779
3830
  }
3780
- const errorText2 = await response.text().catch(() => "unknown error");
3781
- throw new Error(`Embedding API error (${response.status}): ${errorText2}`);
3831
+ const abortSignal = signal;
3832
+ if (abortSignal.aborted) throw new Error("Embedding API retry aborted");
3833
+ await new Promise((resolve, reject) => {
3834
+ const timer = setTimeout(done, waitMs);
3835
+ const onAbort = () => {
3836
+ clearTimeout(timer);
3837
+ abortSignal.removeEventListener("abort", onAbort);
3838
+ reject(new Error("Embedding API retry aborted"));
3839
+ };
3840
+ function done() {
3841
+ abortSignal.removeEventListener("abort", onAbort);
3842
+ resolve();
3843
+ }
3844
+ abortSignal.addEventListener("abort", onAbort, { once: true });
3845
+ });
3782
3846
  }
3783
- 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;
3847
+ 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;
3784
3848
  var init_api_provider = __esm({
3785
3849
  "src/embedding/api-provider.ts"() {
3786
3850
  "use strict";
@@ -3799,6 +3863,14 @@ var init_api_provider = __esm({
3799
3863
  DASHSCOPE_MAX_BATCH_SIZE = 10;
3800
3864
  MAX_RETRIES2 = 3;
3801
3865
  BASE_DELAY_MS = 500;
3866
+ EmbeddingAPIError = class extends Error {
3867
+ status;
3868
+ constructor(status, detail) {
3869
+ super(`Embedding API error (${status}): ${detail}`);
3870
+ this.name = "EmbeddingAPIError";
3871
+ this.status = status;
3872
+ }
3873
+ };
3802
3874
  APIEmbeddingProvider = class _APIEmbeddingProvider {
3803
3875
  name;
3804
3876
  dimensions;
@@ -4010,15 +4082,14 @@ var init_api_provider = __esm({
4010
4082
  }
4011
4083
  } catch (error) {
4012
4084
  const providerLimit = parseBatchLimit(error);
4013
- const fallbackSize = providerLimit ?? Math.ceil(chunkTexts.length / 2);
4014
- if (chunkTexts.length > 1 && fallbackSize < chunkTexts.length) {
4085
+ if (providerLimit !== null && chunkTexts.length > 1 && providerLimit < chunkTexts.length) {
4015
4086
  console.error(
4016
- `[memorix] Embedding batch too large for provider, retrying in chunks of ${fallbackSize}`
4087
+ `[memorix] Embedding batch too large for provider, retrying in chunks of ${providerLimit}`
4017
4088
  );
4018
- for (let start = 0; start < chunkTexts.length; start += fallbackSize) {
4089
+ for (let start = 0; start < chunkTexts.length; start += providerLimit) {
4019
4090
  await processChunk(
4020
- chunkTexts.slice(start, start + fallbackSize),
4021
- chunkIndices.slice(start, start + fallbackSize)
4091
+ chunkTexts.slice(start, start + providerLimit),
4092
+ chunkIndices.slice(start, start + providerLimit)
4022
4093
  );
4023
4094
  }
4024
4095
  return;
@@ -4882,22 +4953,23 @@ function getLLMConfig() {
4882
4953
  function setLLMConfig(config) {
4883
4954
  currentConfig = config;
4884
4955
  }
4885
- async function callLLM(systemPrompt, userMessage) {
4956
+ async function callLLM(systemPrompt, userMessage, signal) {
4886
4957
  if (!currentConfig) {
4887
4958
  throw new Error("LLM not configured. Set MEMORIX_LLM_API_KEY or OPENAI_API_KEY.");
4888
4959
  }
4889
4960
  if (currentConfig.provider === "anthropic") {
4890
- return callAnthropic(systemPrompt, userMessage);
4961
+ return callAnthropic(systemPrompt, userMessage, signal);
4891
4962
  }
4892
- return callOpenAICompatible(systemPrompt, userMessage);
4963
+ return callOpenAICompatible(systemPrompt, userMessage, signal);
4893
4964
  }
4894
- async function callOpenAICompatible(systemPrompt, userMessage) {
4965
+ async function callOpenAICompatible(systemPrompt, userMessage, signal) {
4895
4966
  const config = currentConfig;
4896
4967
  let base = config.baseUrl.replace(/\/+$/, "");
4897
4968
  if (!base.endsWith("/v1")) base += "/v1";
4898
4969
  const url = `${base}/chat/completions`;
4970
+ const fetchSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(LLM_CALL_TIMEOUT_MS)]) : AbortSignal.timeout(LLM_CALL_TIMEOUT_MS);
4899
4971
  const response = await fetch(url, {
4900
- signal: AbortSignal.timeout(LLM_CALL_TIMEOUT_MS),
4972
+ signal: fetchSignal,
4901
4973
  method: "POST",
4902
4974
  headers: {
4903
4975
  "Content-Type": "application/json",
@@ -4914,10 +4986,10 @@ async function callOpenAICompatible(systemPrompt, userMessage) {
4914
4986
  })
4915
4987
  });
4916
4988
  if (!response.ok) {
4917
- const error = await readResponseText(response, void 0, 64 * 1024).catch(() => "unknown error");
4989
+ const error = await readResponseText(response, signal, 64 * 1024).catch(() => "unknown error");
4918
4990
  throw new Error(`LLM API error (${response.status}): ${error}`);
4919
4991
  }
4920
- const data = await readResponseJson(response);
4992
+ const data = await readResponseJson(response, signal);
4921
4993
  return {
4922
4994
  content: data.choices[0]?.message?.content ?? "",
4923
4995
  usage: data.usage ? {
@@ -4926,11 +4998,12 @@ async function callOpenAICompatible(systemPrompt, userMessage) {
4926
4998
  } : void 0
4927
4999
  };
4928
5000
  }
4929
- async function callAnthropic(systemPrompt, userMessage) {
5001
+ async function callAnthropic(systemPrompt, userMessage, signal) {
4930
5002
  const config = currentConfig;
4931
5003
  const url = `${config.baseUrl}/messages`;
5004
+ const fetchSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(LLM_CALL_TIMEOUT_MS)]) : AbortSignal.timeout(LLM_CALL_TIMEOUT_MS);
4932
5005
  const response = await fetch(url, {
4933
- signal: AbortSignal.timeout(LLM_CALL_TIMEOUT_MS),
5006
+ signal: fetchSignal,
4934
5007
  method: "POST",
4935
5008
  headers: {
4936
5009
  "Content-Type": "application/json",
@@ -4948,10 +5021,10 @@ async function callAnthropic(systemPrompt, userMessage) {
4948
5021
  })
4949
5022
  });
4950
5023
  if (!response.ok) {
4951
- const error = await readResponseText(response, void 0, 64 * 1024).catch(() => "unknown error");
5024
+ const error = await readResponseText(response, signal, 64 * 1024).catch(() => "unknown error");
4952
5025
  throw new Error(`Anthropic API error (${response.status}): ${error}`);
4953
5026
  }
4954
- const data = await readResponseJson(response);
5027
+ const data = await readResponseJson(response, signal);
4955
5028
  return {
4956
5029
  content: data.content[0]?.text ?? "",
4957
5030
  usage: data.usage ? {
@@ -5456,6 +5529,57 @@ function withTimeout(promise, ms, label) {
5456
5529
  );
5457
5530
  });
5458
5531
  }
5532
+ function withTimeoutSignal(operation, ms, label, parentSignal) {
5533
+ const controller = new AbortController();
5534
+ const signal = parentSignal ? AbortSignal.any([parentSignal, controller.signal]) : controller.signal;
5535
+ return new Promise((resolve, reject) => {
5536
+ let settled = false;
5537
+ let timedOut = false;
5538
+ let timer;
5539
+ const cleanup = () => {
5540
+ if (timer !== void 0) clearTimeout(timer);
5541
+ parentSignal?.removeEventListener("abort", onParentAbort);
5542
+ };
5543
+ const onParentAbort = () => {
5544
+ if (settled || timedOut) return;
5545
+ settled = true;
5546
+ cleanup();
5547
+ reject(parentSignal?.reason ?? new Error(`${label} aborted`));
5548
+ };
5549
+ parentSignal?.addEventListener("abort", onParentAbort, { once: true });
5550
+ timer = setTimeout(() => {
5551
+ if (settled) return;
5552
+ timedOut = true;
5553
+ controller.abort(new Error(`${label} timed out after ${ms}ms`));
5554
+ settled = true;
5555
+ cleanup();
5556
+ reject(new Error(`${label} timed out after ${ms}ms`));
5557
+ }, ms);
5558
+ let pending;
5559
+ try {
5560
+ pending = operation(signal);
5561
+ } catch (error) {
5562
+ settled = true;
5563
+ cleanup();
5564
+ reject(error);
5565
+ return;
5566
+ }
5567
+ pending.then(
5568
+ (value) => {
5569
+ if (settled) return;
5570
+ settled = true;
5571
+ cleanup();
5572
+ resolve(value);
5573
+ },
5574
+ (error) => {
5575
+ if (settled) return;
5576
+ settled = true;
5577
+ cleanup();
5578
+ reject(timedOut ? new Error(`${label} timed out after ${ms}ms`) : error);
5579
+ }
5580
+ );
5581
+ });
5582
+ }
5459
5583
  var init_timeout = __esm({
5460
5584
  "src/timeout.ts"() {
5461
5585
  "use strict";
@@ -6320,16 +6444,18 @@ async function searchObservations(options) {
6320
6444
  );
6321
6445
  } else {
6322
6446
  const EMBEDDING_TIMEOUT_MS = 15e3;
6323
- queryVector = await withTimeout(
6324
- provider2.embedInput ? provider2.embedInput(
6447
+ queryVector = await withTimeoutSignal(
6448
+ (signal) => provider2.embedInput ? provider2.embedInput(
6325
6449
  { modality: "text", text: expandedEmbeddingQuery },
6326
- { intent: "query", timeoutMs: EMBEDDING_TIMEOUT_MS, retry: false }
6450
+ { intent: "query", timeoutMs: EMBEDDING_TIMEOUT_MS, retry: false, signal }
6327
6451
  ) : provider2.embed(expandedEmbeddingQuery, {
6328
6452
  timeoutMs: EMBEDDING_TIMEOUT_MS,
6329
- retry: false
6453
+ retry: false,
6454
+ signal
6330
6455
  }),
6331
6456
  EMBEDDING_TIMEOUT_MS,
6332
- "Embedding"
6457
+ "Embedding",
6458
+ options.signal
6333
6459
  );
6334
6460
  mark("embedding");
6335
6461
  const cjkRatio = (originalQuery.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g) || []).length / originalQuery.length;
@@ -142927,7 +143053,13 @@ async function maintainLongTermMemories(input) {
142927
143053
  }
142928
143054
  for (const group of groups.values()) {
142929
143055
  if (group.length < 2) continue;
142930
- group.sort((a, b) => Date.parse(b.createdAt) - Date.parse(a.createdAt));
143056
+ group.sort((a, b) => {
143057
+ const byCreatedAt = Date.parse(b.createdAt) - Date.parse(a.createdAt);
143058
+ if (byCreatedAt !== 0) return byCreatedAt;
143059
+ const byUpdatedAt = Date.parse(b.updatedAt) - Date.parse(a.updatedAt);
143060
+ if (byUpdatedAt !== 0) return byUpdatedAt;
143061
+ return b.id.localeCompare(a.id);
143062
+ });
142931
143063
  const newest = group[0];
142932
143064
  for (const older of group.slice(1)) {
142933
143065
  try {