memorix 1.2.4 → 1.2.5

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.
package/dist/index.js CHANGED
@@ -2803,6 +2803,25 @@ function getResolvedConfig(options = {}) {
2803
2803
  const legacy = loadFileConfig();
2804
2804
  const embeddingBaseUrl = first(process.env.MEMORIX_EMBEDDING_BASE_URL, toml.embedding?.base_url, yaml2.embedding?.baseUrl, legacy.embeddingApi?.baseUrl);
2805
2805
  const openRouterEmbeddingApiKey = isOpenRouterUrl(embeddingBaseUrl) ? process.env.OPENROUTER_API_KEY : void 0;
2806
+ const memoryLlmProvider = first(
2807
+ process.env.MEMORIX_LLM_PROVIDER,
2808
+ toml.memory?.llm?.provider,
2809
+ yaml2.llm?.provider,
2810
+ legacy.llm?.provider
2811
+ );
2812
+ const memoryLlmModel = first(
2813
+ process.env.MEMORIX_LLM_MODEL,
2814
+ toml.memory?.llm?.model,
2815
+ yaml2.llm?.model,
2816
+ legacy.llm?.model
2817
+ );
2818
+ const memoryLlmBaseUrl = first(
2819
+ process.env.MEMORIX_LLM_BASE_URL,
2820
+ toml.memory?.llm?.base_url,
2821
+ yaml2.llm?.baseUrl,
2822
+ legacy.llm?.baseUrl
2823
+ );
2824
+ const openRouterMemoryLlmApiKey = isOpenRouterMemoryLane(memoryLlmProvider, memoryLlmBaseUrl) ? process.env.OPENROUTER_API_KEY : void 0;
2806
2825
  const resolved = {
2807
2826
  agent: {
2808
2827
  provider: first(
@@ -2840,9 +2859,9 @@ function getResolvedConfig(options = {}) {
2840
2859
  autoCleanup: firstBool(toml.memory?.auto_cleanup, yaml2.behavior?.autoCleanup),
2841
2860
  syncAdvisory: firstBool(toml.memory?.sync_advisory, yaml2.behavior?.syncAdvisory),
2842
2861
  llm: {
2843
- provider: first(process.env.MEMORIX_LLM_PROVIDER, toml.memory?.llm?.provider, yaml2.llm?.provider, legacy.llm?.provider),
2844
- model: first(process.env.MEMORIX_LLM_MODEL, toml.memory?.llm?.model, yaml2.llm?.model, legacy.llm?.model),
2845
- baseUrl: first(process.env.MEMORIX_LLM_BASE_URL, toml.memory?.llm?.base_url, yaml2.llm?.baseUrl, legacy.llm?.baseUrl),
2862
+ provider: memoryLlmProvider,
2863
+ model: memoryLlmModel,
2864
+ baseUrl: memoryLlmBaseUrl,
2846
2865
  apiKey: first(
2847
2866
  process.env.MEMORIX_LLM_API_KEY,
2848
2867
  process.env.MEMORIX_API_KEY,
@@ -2851,7 +2870,7 @@ function getResolvedConfig(options = {}) {
2851
2870
  legacy.llm?.apiKey,
2852
2871
  process.env.OPENAI_API_KEY,
2853
2872
  process.env.ANTHROPIC_API_KEY,
2854
- process.env.OPENROUTER_API_KEY
2873
+ openRouterMemoryLlmApiKey
2855
2874
  )
2856
2875
  }
2857
2876
  },
@@ -2988,6 +3007,9 @@ function isOpenRouterUrl(value) {
2988
3007
  return /(^|\.)openrouter\.ai(?::|\/|$)/i.test(value);
2989
3008
  }
2990
3009
  }
3010
+ function isOpenRouterMemoryLane(provider2, baseUrl) {
3011
+ return provider2?.trim().toLowerCase() === "openrouter" || isOpenRouterUrl(baseUrl);
3012
+ }
2991
3013
  function normalizeExternalContext(value) {
2992
3014
  const normalized = value?.trim().toLowerCase();
2993
3015
  if (normalized === "auto" || normalized === "off") return normalized;
@@ -5035,6 +5057,29 @@ Rules:
5035
5057
  }
5036
5058
  });
5037
5059
 
5060
+ // src/timeout.ts
5061
+ function withTimeout(promise, ms, label) {
5062
+ return new Promise((resolve2, reject) => {
5063
+ const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
5064
+ promise.then(
5065
+ (value) => {
5066
+ clearTimeout(timer);
5067
+ resolve2(value);
5068
+ },
5069
+ (error) => {
5070
+ clearTimeout(timer);
5071
+ reject(error);
5072
+ }
5073
+ );
5074
+ });
5075
+ }
5076
+ var init_timeout = __esm({
5077
+ "src/timeout.ts"() {
5078
+ "use strict";
5079
+ init_esm_shims();
5080
+ }
5081
+ });
5082
+
5038
5083
  // src/project/aliases.ts
5039
5084
  var aliases_exports = {};
5040
5085
  __export(aliases_exports, {
@@ -5800,8 +5845,12 @@ async function searchObservations(options) {
5800
5845
  }
5801
5846
  };
5802
5847
  const modeKey = options.projectId ?? SEARCH_MODE_DEFAULT_KEY;
5803
- lastSearchModeByProject.set(modeKey, embeddingEnabled ? "hybrid" : "fulltext");
5848
+ const quality = options.quality ?? "balanced";
5804
5849
  const database = await getDb();
5850
+ lastSearchModeByProject.set(
5851
+ modeKey,
5852
+ quality === "fast" ? "fulltext (fast profile)" : embeddingEnabled ? "hybrid" : "fulltext"
5853
+ );
5805
5854
  let projectIds = null;
5806
5855
  if (options.projectId) {
5807
5856
  try {
@@ -5823,11 +5872,15 @@ async function searchObservations(options) {
5823
5872
  }
5824
5873
  const hasQuery = options.query && options.query.trim().length > 0;
5825
5874
  const originalQuery = options.query;
5826
- const tier = hasQuery ? classifyQueryTier(originalQuery) : "fast";
5875
+ const tier = quality === "fast" ? "fast" : hasQuery ? classifyQueryTier(originalQuery) : "fast";
5827
5876
  mark(`tier=${tier}`);
5828
- const expandedEmbeddingQuery = tier === "heavy" ? await maybeExpandSearchQuery(options.query) : options.query;
5877
+ if (quality === "thorough" && hasQuery) {
5878
+ const { initLLM: initLLM2, isLLMEnabled: isLLMEnabled2 } = await Promise.resolve().then(() => (init_provider2(), provider_exports2));
5879
+ if (!isLLMEnabled2()) initLLM2();
5880
+ }
5881
+ const expandedEmbeddingQuery = quality === "thorough" && tier === "heavy" ? await maybeExpandSearchQuery(options.query) : options.query;
5829
5882
  mark("queryExpansion");
5830
- const intentResult = hasQuery ? detectQueryIntent(originalQuery) : null;
5883
+ const intentResult = quality === "fast" || !hasQuery ? null : detectQueryIntent(originalQuery);
5831
5884
  const requestLimit = projectIds ? (options.limit ?? 20) * 3 : options.limit ?? 20;
5832
5885
  const defaultBoost = {
5833
5886
  title: 3,
@@ -5841,17 +5894,21 @@ async function searchObservations(options) {
5841
5894
  let searchParams = {
5842
5895
  term: originalQuery,
5843
5896
  limit: requestLimit,
5844
- includeVectors: true,
5897
+ // Fast search never uses vectors, so returning them only adds work and
5898
+ // payload pressure to an explicitly latency-sensitive path.
5899
+ includeVectors: quality !== "fast",
5845
5900
  ...Object.keys(filters).length > 0 ? { where: filters } : {},
5846
5901
  // Search specific fields (not tokens, accessCount, etc.)
5847
5902
  properties: ["title", "entityName", "narrative", "facts", "concepts", "filesModified"],
5848
5903
  // Field boosting: intent-aware or default
5849
5904
  boost: fieldBoost,
5850
5905
  // Fuzzy tolerance: allow 1-char typos for short queries, 2 for longer
5851
- ...hasQuery ? { tolerance: originalQuery.length > 6 ? 2 : 1 } : {}
5906
+ // Fuzzy matching is useful for ordinary recall, but grows sharply with
5907
+ // corpus size. The fast profile deliberately uses exact lexical matching.
5908
+ ...hasQuery && quality !== "fast" ? { tolerance: originalQuery.length > 6 ? 2 : 1 } : {}
5852
5909
  };
5853
5910
  let queryVector = null;
5854
- if (embeddingEnabled && hasQuery && tier !== "fast") {
5911
+ if (quality !== "fast" && embeddingEnabled && hasQuery && tier !== "fast") {
5855
5912
  try {
5856
5913
  const provider2 = await getEmbeddingProvider();
5857
5914
  if (provider2) {
@@ -5866,11 +5923,11 @@ async function searchObservations(options) {
5866
5923
  );
5867
5924
  } else {
5868
5925
  const EMBEDDING_TIMEOUT_MS = 15e3;
5869
- const embedPromise = provider2.embed(expandedEmbeddingQuery);
5870
- const timeoutPromise = new Promise(
5871
- (_, reject) => setTimeout(() => reject(new Error(`Embedding timeout after ${EMBEDDING_TIMEOUT_MS}ms`)), EMBEDDING_TIMEOUT_MS)
5926
+ queryVector = await withTimeout(
5927
+ provider2.embed(expandedEmbeddingQuery),
5928
+ EMBEDDING_TIMEOUT_MS,
5929
+ "Embedding"
5872
5930
  );
5873
- queryVector = await Promise.race([embedPromise, timeoutPromise]);
5874
5931
  mark("embedding");
5875
5932
  const cjkRatio = (originalQuery.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g) || []).length / originalQuery.length;
5876
5933
  const isCJKHeavy = cjkRatio > 0.3;
@@ -6111,7 +6168,7 @@ async function searchObservations(options) {
6111
6168
  }
6112
6169
  }
6113
6170
  }
6114
- const shouldRerank = tier === "heavy" && hasQuery && intermediate.length > 2 && (() => {
6171
+ const shouldRerank = quality === "thorough" && tier === "heavy" && hasQuery && intermediate.length > 2 && (() => {
6115
6172
  const top = intermediate[0]?.score ?? 0;
6116
6173
  const second = intermediate[1]?.score ?? 0;
6117
6174
  return top > 0 && second / top > 0.7;
@@ -6135,11 +6192,11 @@ async function searchObservations(options) {
6135
6192
  }));
6136
6193
  const _parsedRerank = parseInt(process.env.MEMORIX_RERANK_TIMEOUT_MS || "", 10);
6137
6194
  const RERANK_TIMEOUT_MS = Number.isFinite(_parsedRerank) && _parsedRerank > 0 ? _parsedRerank : 5e3;
6138
- const rerankPromise = rerankResults2(originalQuery, candidates);
6139
- const timeoutPromise = new Promise(
6140
- (_, reject) => setTimeout(() => reject(new Error(`LLM rerank timeout after ${RERANK_TIMEOUT_MS}ms`)), RERANK_TIMEOUT_MS)
6195
+ const { reranked, usedLLM } = await withTimeout(
6196
+ rerankResults2(originalQuery, candidates),
6197
+ RERANK_TIMEOUT_MS,
6198
+ "LLM rerank"
6141
6199
  );
6142
- const { reranked, usedLLM } = await Promise.race([rerankPromise, timeoutPromise]);
6143
6200
  mark(`rerank(usedLLM=${usedLLM})`);
6144
6201
  if (usedLLM) {
6145
6202
  lastSearchModeByProject.set(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + LLM rerank");
@@ -6331,6 +6388,7 @@ var init_orama_store = __esm({
6331
6388
  init_project_affinity();
6332
6389
  init_intent_detector();
6333
6390
  init_query_expansion();
6391
+ init_timeout();
6334
6392
  db = null;
6335
6393
  dbInitPromise = null;
6336
6394
  dbGeneration = 0;
@@ -14855,21 +14913,6 @@ async function loadWorkspaceForMaintenance(projectId, projectDir2, mode) {
14855
14913
  ]);
14856
14914
  return versioned ?? local;
14857
14915
  }
14858
- function withTimeout(promise, ms, label) {
14859
- return new Promise((resolve2, reject) => {
14860
- const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
14861
- promise.then(
14862
- (value) => {
14863
- clearTimeout(timer);
14864
- resolve2(value);
14865
- },
14866
- (error) => {
14867
- clearTimeout(timer);
14868
- reject(error);
14869
- }
14870
- );
14871
- });
14872
- }
14873
14916
  async function runAutomaticConsolidation(projectId, projectDir2, options) {
14874
14917
  const { isLLMEnabled: isLLMEnabled2 } = await Promise.resolve().then(() => (init_provider2(), provider_exports2));
14875
14918
  if (!isLLMEnabled2()) {
@@ -15254,6 +15297,7 @@ var init_project_maintenance = __esm({
15254
15297
  init_esm_shims();
15255
15298
  init_isolated_maintenance();
15256
15299
  init_lifecycle();
15300
+ init_timeout();
15257
15301
  DEFAULT_VECTOR_BATCH_SIZE = 12;
15258
15302
  DEFAULT_RETENTION_BATCH_SIZE = 100;
15259
15303
  DEFAULT_CONSOLIDATION_BATCH_SIZE = 200;
@@ -26889,18 +26933,10 @@ function parseFormationTimeoutMs(raw) {
26889
26933
  }
26890
26934
 
26891
26935
  // src/server.ts
26936
+ init_timeout();
26892
26937
  var FORMATION_TIMEOUT_MS = parseFormationTimeoutMs(process.env.MEMORIX_FORMATION_TIMEOUT_MS);
26893
26938
  var COMPACT_ON_WRITE_TIMEOUT_MS = 12e3;
26894
26939
  var COMPRESSION_TIMEOUT_MS = 5e3;
26895
- function withTimeout2(promise, ms, label) {
26896
- let timer;
26897
- return Promise.race([
26898
- promise,
26899
- new Promise((_, reject) => {
26900
- timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
26901
- })
26902
- ]).finally(() => clearTimeout(timer));
26903
- }
26904
26940
  function formatFormationStageDurations(stageDurationsMs) {
26905
26941
  const orderedStages = ["extract", "resolve", "evaluate"];
26906
26942
  const parts = orderedStages.filter((stage) => stageDurationsMs[stage] !== void 0).map((stage) => `${stage}=${stageDurationsMs[stage]}ms`);
@@ -27276,7 +27312,7 @@ The path should point to a directory containing a .git folder.`
27276
27312
  };
27277
27313
  const server = existingServer ?? new McpServer({
27278
27314
  name: "memorix",
27279
- version: true ? "1.2.4" : "1.0.1"
27315
+ version: true ? "1.2.5" : "1.0.1"
27280
27316
  });
27281
27317
  const originalRegisterTool = server.registerTool.bind(server);
27282
27318
  server.registerTool = ((name, ...args) => {
@@ -27473,7 +27509,7 @@ The path should point to a directory containing a .git folder.`
27473
27509
  getEntityNames: () => graphManager.getEntityNames(),
27474
27510
  onStageEvent: onFormationStageEvent
27475
27511
  };
27476
- formationResult = await withTimeout2(
27512
+ formationResult = await withTimeout(
27477
27513
  runFormation({
27478
27514
  entityName,
27479
27515
  type,
@@ -27594,7 +27630,7 @@ ${modeIcon} Formation[active]: ${formationResult.evaluation.category} (${formati
27594
27630
  facts: d.facts,
27595
27631
  score: similarEntries[i]?.score ?? 0
27596
27632
  }));
27597
- const decision = await withTimeout2(
27633
+ const decision = await withTimeout(
27598
27634
  compactOnWrite(
27599
27635
  { title, narrative, facts: safeFacts ?? [] },
27600
27636
  existingMemories
@@ -27690,7 +27726,7 @@ Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
27690
27726
  let compressionNote = "";
27691
27727
  try {
27692
27728
  const { compressNarrative: compressNarrative2 } = await Promise.resolve().then(() => (init_quality(), quality_exports));
27693
- const { compressed, saved, usedLLM } = await withTimeout2(
27729
+ const { compressed, saved, usedLLM } = await withTimeout(
27694
27730
  compressNarrative2(narrative, safeFacts, type),
27695
27731
  COMPRESSION_TIMEOUT_MS,
27696
27732
  "Narrative compression"
@@ -27812,7 +27848,7 @@ Auto-enriched: ${enrichmentParts.join(", ")}` : "";
27812
27848
  },
27813
27849
  getEntityNames: () => graphManager.getEntityNames()
27814
27850
  };
27815
- const formed = await withTimeout2(
27851
+ const formed = await withTimeout(
27816
27852
  runFormation({ entityName, type, title, narrative, facts: safeFacts, projectId: project.id, source: "explicit", topicKey }, formationConfig),
27817
27853
  FORMATION_TIMEOUT_MS,
27818
27854
  "Shadow formation"
@@ -27903,6 +27939,9 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27903
27939
  source: z2.enum(["agent", "git", "manual"]).optional().describe(
27904
27940
  'Filter by memory source. "git" returns only commit-derived ground truth memories. Omit for all sources.'
27905
27941
  ),
27942
+ quality: z2.enum(["fast", "balanced", "thorough"]).optional().default("balanced").describe(
27943
+ "Retrieval profile: fast stays local, balanced uses configured embeddings, thorough explicitly permits optional LLM refinement."
27944
+ ),
27906
27945
  purpose: z2.string().optional().describe(
27907
27946
  "Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user's explicit request."
27908
27947
  ),
@@ -27911,7 +27950,7 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27911
27950
  )
27912
27951
  }
27913
27952
  },
27914
- async ({ query, limit, type, maxTokens, scope, since, until, status, source, purpose, force }) => {
27953
+ async ({ query, limit, type, maxTokens, scope, since, until, status, source, quality, purpose, force }) => {
27915
27954
  if (scope !== "global") {
27916
27955
  const unresolved = requireResolvedProject("search the current project");
27917
27956
  if (unresolved) return unresolved;
@@ -27936,16 +27975,14 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27936
27975
  projectId: scope === "global" ? void 0 : project.id,
27937
27976
  status: status ?? "active",
27938
27977
  source,
27978
+ quality,
27939
27979
  reader: getObservationReader(scope === "global" ? "global" : "project")
27940
27980
  });
27941
- const timeoutPromise = new Promise(
27942
- (_, reject) => setTimeout(() => reject(new Error(`Search timeout after ${TIMEOUT_MS}ms`)), TIMEOUT_MS)
27943
- );
27944
27981
  let result;
27945
27982
  try {
27946
- result = await Promise.race([searchPromise, timeoutPromise]);
27983
+ result = await withTimeout(searchPromise, TIMEOUT_MS, "Search");
27947
27984
  } catch (error) {
27948
- if (error instanceof Error && error.message.includes("timeout")) {
27985
+ if (error instanceof Error && /\btimeout\b|timed out/i.test(error.message)) {
27949
27986
  return {
27950
27987
  content: [
27951
27988
  {