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.
@@ -2,6 +2,19 @@
2
2
 
3
3
  All notable changes to this project will be documented in this file.
4
4
 
5
+ ## [1.2.5] - 2026-07-27
6
+
7
+ ### Added
8
+ - **Explicit retrieval profiles** -- Search now accepts `fast`, `balanced` (default), or `thorough` through the CLI, MCP, and SDK. Fast is fully local, balanced keeps optional LLM work out of everyday retrieval, and thorough explicitly opts into configured LLM query refinement.
9
+ - **Reproducible retrieval evidence** -- Added `npm run benchmark:retrieval` for a clearly-labelled hot in-process SDK lexical benchmark with p50/p95/p99 output. It does not present CLI startup, MCP transport, remote embedding, or LLM latency as the same metric.
10
+
11
+ ### Changed
12
+ - **Lighter read-only CLI path** -- Memory search, detail, timeline, recent, graph context, and help no longer create session bookkeeping or maintenance-target metadata. Identity and visibility checks remain unchanged.
13
+
14
+ ### Fixed
15
+ - **Completed searches no longer retain timeout watchdogs** -- Embedding, rerank, MCP search, and maintenance timeout paths share a helper that clears the timer when work settles. This removes the avoidable CLI exit delay caused by an already-completed optional rerank.
16
+ - **OpenRouter lane isolation** -- An embedding-only `OPENROUTER_API_KEY` no longer silently enables the memory LLM lane. It remains valid for a memory LLM only when that lane explicitly selects an OpenRouter provider or base URL.
17
+
5
18
  ## [1.2.4] - 2026-07-26
6
19
 
7
20
  ### Added
package/dist/sdk.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Entity, Relation, KnowledgeGraph, ProjectInfo, DetectionResult, ObservationType, ObservationStatus, ProgressInfo, Observation, IndexEntry } from './types.js';
1
+ import { Entity, Relation, KnowledgeGraph, ProjectInfo, DetectionResult, ObservationType, ObservationStatus, RetrievalQuality, ProgressInfo, Observation, IndexEntry } from './types.js';
2
2
  export { AgentTarget, DEFAULT_CONFIG, DetectionFailure, DetectionFailureReason, DocumentType, KnowledgeLayer, MCPConfigAdapter, MCPServerEntry, MemorixConfig, MemorixDocument, MemoryRef, MiniSkill, OBSERVATION_ICONS, ObservationAdmissionState, ObservationReader, ObservationRef, ObservationVisibility, RuleFormatAdapter, RuleSource, SearchOptions, Session, SkillConflict, SkillEntry, SnapshotObservation, SourceSnapshot, TOPIC_KEY_FAMILIES, TimelineContext, UnifiedRule, WorkflowEntry, WorkspaceSyncResult } from './types.js';
3
3
  import { McpServer } from '@modelcontextprotocol/sdk/server/mcp.js';
4
4
 
@@ -559,6 +559,8 @@ interface ClientSearchOptions {
559
559
  status?: ObservationStatus | 'all';
560
560
  /** Maximum results. Default: 20 */
561
561
  limit?: number;
562
+ /** Retrieval profile. Default: balanced. */
563
+ quality?: RetrievalQuality;
562
564
  }
563
565
  /** Result from resolving observations */
564
566
  interface ResolveResult {
@@ -687,4 +689,4 @@ declare class MemoryClient {
687
689
  */
688
690
  declare function createMemoryClient(options: MemoryClientOptions): Promise<MemoryClient>;
689
691
 
690
- export { type ClientSearchOptions, type CreateMemorixServerOptions, DetectionResult, Entity, IndexEntry, KnowledgeGraph, MemoryClient, type MemoryClientOptions, Observation, ObservationStatus, ObservationType, ProgressInfo, ProjectInfo, Relation, type ResolveResult, type StoreInput, type StoreResult, createMemorixServer, createMemoryClient, detectProject, detectProjectWithDiagnostics };
692
+ export { type ClientSearchOptions, type CreateMemorixServerOptions, DetectionResult, Entity, IndexEntry, KnowledgeGraph, MemoryClient, type MemoryClientOptions, Observation, ObservationStatus, ObservationType, ProgressInfo, ProjectInfo, Relation, type ResolveResult, RetrievalQuality, type StoreInput, type StoreResult, createMemorixServer, createMemoryClient, detectProject, detectProjectWithDiagnostics };
package/dist/sdk.js CHANGED
@@ -2807,6 +2807,25 @@ function getResolvedConfig(options = {}) {
2807
2807
  const legacy = loadFileConfig();
2808
2808
  const embeddingBaseUrl = first(process.env.MEMORIX_EMBEDDING_BASE_URL, toml.embedding?.base_url, yaml2.embedding?.baseUrl, legacy.embeddingApi?.baseUrl);
2809
2809
  const openRouterEmbeddingApiKey = isOpenRouterUrl(embeddingBaseUrl) ? process.env.OPENROUTER_API_KEY : void 0;
2810
+ const memoryLlmProvider = first(
2811
+ process.env.MEMORIX_LLM_PROVIDER,
2812
+ toml.memory?.llm?.provider,
2813
+ yaml2.llm?.provider,
2814
+ legacy.llm?.provider
2815
+ );
2816
+ const memoryLlmModel = first(
2817
+ process.env.MEMORIX_LLM_MODEL,
2818
+ toml.memory?.llm?.model,
2819
+ yaml2.llm?.model,
2820
+ legacy.llm?.model
2821
+ );
2822
+ const memoryLlmBaseUrl = first(
2823
+ process.env.MEMORIX_LLM_BASE_URL,
2824
+ toml.memory?.llm?.base_url,
2825
+ yaml2.llm?.baseUrl,
2826
+ legacy.llm?.baseUrl
2827
+ );
2828
+ const openRouterMemoryLlmApiKey = isOpenRouterMemoryLane(memoryLlmProvider, memoryLlmBaseUrl) ? process.env.OPENROUTER_API_KEY : void 0;
2810
2829
  const resolved = {
2811
2830
  agent: {
2812
2831
  provider: first(
@@ -2844,9 +2863,9 @@ function getResolvedConfig(options = {}) {
2844
2863
  autoCleanup: firstBool(toml.memory?.auto_cleanup, yaml2.behavior?.autoCleanup),
2845
2864
  syncAdvisory: firstBool(toml.memory?.sync_advisory, yaml2.behavior?.syncAdvisory),
2846
2865
  llm: {
2847
- provider: first(process.env.MEMORIX_LLM_PROVIDER, toml.memory?.llm?.provider, yaml2.llm?.provider, legacy.llm?.provider),
2848
- model: first(process.env.MEMORIX_LLM_MODEL, toml.memory?.llm?.model, yaml2.llm?.model, legacy.llm?.model),
2849
- baseUrl: first(process.env.MEMORIX_LLM_BASE_URL, toml.memory?.llm?.base_url, yaml2.llm?.baseUrl, legacy.llm?.baseUrl),
2866
+ provider: memoryLlmProvider,
2867
+ model: memoryLlmModel,
2868
+ baseUrl: memoryLlmBaseUrl,
2850
2869
  apiKey: first(
2851
2870
  process.env.MEMORIX_LLM_API_KEY,
2852
2871
  process.env.MEMORIX_API_KEY,
@@ -2855,7 +2874,7 @@ function getResolvedConfig(options = {}) {
2855
2874
  legacy.llm?.apiKey,
2856
2875
  process.env.OPENAI_API_KEY,
2857
2876
  process.env.ANTHROPIC_API_KEY,
2858
- process.env.OPENROUTER_API_KEY
2877
+ openRouterMemoryLlmApiKey
2859
2878
  )
2860
2879
  }
2861
2880
  },
@@ -2992,6 +3011,9 @@ function isOpenRouterUrl(value) {
2992
3011
  return /(^|\.)openrouter\.ai(?::|\/|$)/i.test(value);
2993
3012
  }
2994
3013
  }
3014
+ function isOpenRouterMemoryLane(provider2, baseUrl) {
3015
+ return provider2?.trim().toLowerCase() === "openrouter" || isOpenRouterUrl(baseUrl);
3016
+ }
2995
3017
  function normalizeExternalContext(value) {
2996
3018
  const normalized = value?.trim().toLowerCase();
2997
3019
  if (normalized === "auto" || normalized === "off") return normalized;
@@ -5039,6 +5061,29 @@ Rules:
5039
5061
  }
5040
5062
  });
5041
5063
 
5064
+ // src/timeout.ts
5065
+ function withTimeout(promise, ms, label) {
5066
+ return new Promise((resolve2, reject) => {
5067
+ const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
5068
+ promise.then(
5069
+ (value) => {
5070
+ clearTimeout(timer);
5071
+ resolve2(value);
5072
+ },
5073
+ (error) => {
5074
+ clearTimeout(timer);
5075
+ reject(error);
5076
+ }
5077
+ );
5078
+ });
5079
+ }
5080
+ var init_timeout = __esm({
5081
+ "src/timeout.ts"() {
5082
+ "use strict";
5083
+ init_esm_shims();
5084
+ }
5085
+ });
5086
+
5042
5087
  // src/project/aliases.ts
5043
5088
  var aliases_exports = {};
5044
5089
  __export(aliases_exports, {
@@ -5804,8 +5849,12 @@ async function searchObservations(options) {
5804
5849
  }
5805
5850
  };
5806
5851
  const modeKey = options.projectId ?? SEARCH_MODE_DEFAULT_KEY;
5807
- lastSearchModeByProject.set(modeKey, embeddingEnabled ? "hybrid" : "fulltext");
5852
+ const quality = options.quality ?? "balanced";
5808
5853
  const database = await getDb();
5854
+ lastSearchModeByProject.set(
5855
+ modeKey,
5856
+ quality === "fast" ? "fulltext (fast profile)" : embeddingEnabled ? "hybrid" : "fulltext"
5857
+ );
5809
5858
  let projectIds = null;
5810
5859
  if (options.projectId) {
5811
5860
  try {
@@ -5827,11 +5876,15 @@ async function searchObservations(options) {
5827
5876
  }
5828
5877
  const hasQuery = options.query && options.query.trim().length > 0;
5829
5878
  const originalQuery = options.query;
5830
- const tier = hasQuery ? classifyQueryTier(originalQuery) : "fast";
5879
+ const tier = quality === "fast" ? "fast" : hasQuery ? classifyQueryTier(originalQuery) : "fast";
5831
5880
  mark(`tier=${tier}`);
5832
- const expandedEmbeddingQuery = tier === "heavy" ? await maybeExpandSearchQuery(options.query) : options.query;
5881
+ if (quality === "thorough" && hasQuery) {
5882
+ const { initLLM: initLLM2, isLLMEnabled: isLLMEnabled2 } = await Promise.resolve().then(() => (init_provider2(), provider_exports2));
5883
+ if (!isLLMEnabled2()) initLLM2();
5884
+ }
5885
+ const expandedEmbeddingQuery = quality === "thorough" && tier === "heavy" ? await maybeExpandSearchQuery(options.query) : options.query;
5833
5886
  mark("queryExpansion");
5834
- const intentResult = hasQuery ? detectQueryIntent(originalQuery) : null;
5887
+ const intentResult = quality === "fast" || !hasQuery ? null : detectQueryIntent(originalQuery);
5835
5888
  const requestLimit = projectIds ? (options.limit ?? 20) * 3 : options.limit ?? 20;
5836
5889
  const defaultBoost = {
5837
5890
  title: 3,
@@ -5845,17 +5898,21 @@ async function searchObservations(options) {
5845
5898
  let searchParams = {
5846
5899
  term: originalQuery,
5847
5900
  limit: requestLimit,
5848
- includeVectors: true,
5901
+ // Fast search never uses vectors, so returning them only adds work and
5902
+ // payload pressure to an explicitly latency-sensitive path.
5903
+ includeVectors: quality !== "fast",
5849
5904
  ...Object.keys(filters).length > 0 ? { where: filters } : {},
5850
5905
  // Search specific fields (not tokens, accessCount, etc.)
5851
5906
  properties: ["title", "entityName", "narrative", "facts", "concepts", "filesModified"],
5852
5907
  // Field boosting: intent-aware or default
5853
5908
  boost: fieldBoost,
5854
5909
  // Fuzzy tolerance: allow 1-char typos for short queries, 2 for longer
5855
- ...hasQuery ? { tolerance: originalQuery.length > 6 ? 2 : 1 } : {}
5910
+ // Fuzzy matching is useful for ordinary recall, but grows sharply with
5911
+ // corpus size. The fast profile deliberately uses exact lexical matching.
5912
+ ...hasQuery && quality !== "fast" ? { tolerance: originalQuery.length > 6 ? 2 : 1 } : {}
5856
5913
  };
5857
5914
  let queryVector = null;
5858
- if (embeddingEnabled && hasQuery && tier !== "fast") {
5915
+ if (quality !== "fast" && embeddingEnabled && hasQuery && tier !== "fast") {
5859
5916
  try {
5860
5917
  const provider2 = await getEmbeddingProvider();
5861
5918
  if (provider2) {
@@ -5870,11 +5927,11 @@ async function searchObservations(options) {
5870
5927
  );
5871
5928
  } else {
5872
5929
  const EMBEDDING_TIMEOUT_MS = 15e3;
5873
- const embedPromise = provider2.embed(expandedEmbeddingQuery);
5874
- const timeoutPromise = new Promise(
5875
- (_, reject) => setTimeout(() => reject(new Error(`Embedding timeout after ${EMBEDDING_TIMEOUT_MS}ms`)), EMBEDDING_TIMEOUT_MS)
5930
+ queryVector = await withTimeout(
5931
+ provider2.embed(expandedEmbeddingQuery),
5932
+ EMBEDDING_TIMEOUT_MS,
5933
+ "Embedding"
5876
5934
  );
5877
- queryVector = await Promise.race([embedPromise, timeoutPromise]);
5878
5935
  mark("embedding");
5879
5936
  const cjkRatio = (originalQuery.match(/[\u4e00-\u9fff\u3040-\u309f\u30a0-\u30ff\uac00-\ud7af]/g) || []).length / originalQuery.length;
5880
5937
  const isCJKHeavy = cjkRatio > 0.3;
@@ -6115,7 +6172,7 @@ async function searchObservations(options) {
6115
6172
  }
6116
6173
  }
6117
6174
  }
6118
- const shouldRerank = tier === "heavy" && hasQuery && intermediate.length > 2 && (() => {
6175
+ const shouldRerank = quality === "thorough" && tier === "heavy" && hasQuery && intermediate.length > 2 && (() => {
6119
6176
  const top = intermediate[0]?.score ?? 0;
6120
6177
  const second = intermediate[1]?.score ?? 0;
6121
6178
  return top > 0 && second / top > 0.7;
@@ -6139,11 +6196,11 @@ async function searchObservations(options) {
6139
6196
  }));
6140
6197
  const _parsedRerank = parseInt(process.env.MEMORIX_RERANK_TIMEOUT_MS || "", 10);
6141
6198
  const RERANK_TIMEOUT_MS = Number.isFinite(_parsedRerank) && _parsedRerank > 0 ? _parsedRerank : 5e3;
6142
- const rerankPromise = rerankResults2(originalQuery, candidates);
6143
- const timeoutPromise = new Promise(
6144
- (_, reject) => setTimeout(() => reject(new Error(`LLM rerank timeout after ${RERANK_TIMEOUT_MS}ms`)), RERANK_TIMEOUT_MS)
6199
+ const { reranked, usedLLM } = await withTimeout(
6200
+ rerankResults2(originalQuery, candidates),
6201
+ RERANK_TIMEOUT_MS,
6202
+ "LLM rerank"
6145
6203
  );
6146
- const { reranked, usedLLM } = await Promise.race([rerankPromise, timeoutPromise]);
6147
6204
  mark(`rerank(usedLLM=${usedLLM})`);
6148
6205
  if (usedLLM) {
6149
6206
  lastSearchModeByProject.set(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + LLM rerank");
@@ -6335,6 +6392,7 @@ var init_orama_store = __esm({
6335
6392
  init_project_affinity();
6336
6393
  init_intent_detector();
6337
6394
  init_query_expansion();
6395
+ init_timeout();
6338
6396
  db = null;
6339
6397
  dbInitPromise = null;
6340
6398
  dbGeneration = 0;
@@ -14859,21 +14917,6 @@ async function loadWorkspaceForMaintenance(projectId, projectDir2, mode) {
14859
14917
  ]);
14860
14918
  return versioned ?? local;
14861
14919
  }
14862
- function withTimeout(promise, ms, label) {
14863
- return new Promise((resolve2, reject) => {
14864
- const timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
14865
- promise.then(
14866
- (value) => {
14867
- clearTimeout(timer);
14868
- resolve2(value);
14869
- },
14870
- (error) => {
14871
- clearTimeout(timer);
14872
- reject(error);
14873
- }
14874
- );
14875
- });
14876
- }
14877
14920
  async function runAutomaticConsolidation(projectId, projectDir2, options) {
14878
14921
  const { isLLMEnabled: isLLMEnabled2 } = await Promise.resolve().then(() => (init_provider2(), provider_exports2));
14879
14922
  if (!isLLMEnabled2()) {
@@ -15258,6 +15301,7 @@ var init_project_maintenance = __esm({
15258
15301
  init_esm_shims();
15259
15302
  init_isolated_maintenance();
15260
15303
  init_lifecycle();
15304
+ init_timeout();
15261
15305
  DEFAULT_VECTOR_BATCH_SIZE = 12;
15262
15306
  DEFAULT_RETENTION_BATCH_SIZE = 100;
15263
15307
  DEFAULT_CONSOLIDATION_BATCH_SIZE = 200;
@@ -26805,18 +26849,10 @@ function parseFormationTimeoutMs(raw) {
26805
26849
  }
26806
26850
 
26807
26851
  // src/server.ts
26852
+ init_timeout();
26808
26853
  var FORMATION_TIMEOUT_MS = parseFormationTimeoutMs(process.env.MEMORIX_FORMATION_TIMEOUT_MS);
26809
26854
  var COMPACT_ON_WRITE_TIMEOUT_MS = 12e3;
26810
26855
  var COMPRESSION_TIMEOUT_MS = 5e3;
26811
- function withTimeout2(promise, ms, label) {
26812
- let timer;
26813
- return Promise.race([
26814
- promise,
26815
- new Promise((_, reject) => {
26816
- timer = setTimeout(() => reject(new Error(`${label} timed out after ${ms}ms`)), ms);
26817
- })
26818
- ]).finally(() => clearTimeout(timer));
26819
- }
26820
26856
  function formatFormationStageDurations(stageDurationsMs) {
26821
26857
  const orderedStages = ["extract", "resolve", "evaluate"];
26822
26858
  const parts = orderedStages.filter((stage) => stageDurationsMs[stage] !== void 0).map((stage) => `${stage}=${stageDurationsMs[stage]}ms`);
@@ -27192,7 +27228,7 @@ The path should point to a directory containing a .git folder.`
27192
27228
  };
27193
27229
  const server = existingServer ?? new McpServer({
27194
27230
  name: "memorix",
27195
- version: true ? "1.2.4" : "1.0.1"
27231
+ version: true ? "1.2.5" : "1.0.1"
27196
27232
  });
27197
27233
  const originalRegisterTool = server.registerTool.bind(server);
27198
27234
  server.registerTool = ((name, ...args) => {
@@ -27389,7 +27425,7 @@ The path should point to a directory containing a .git folder.`
27389
27425
  getEntityNames: () => graphManager.getEntityNames(),
27390
27426
  onStageEvent: onFormationStageEvent
27391
27427
  };
27392
- formationResult = await withTimeout2(
27428
+ formationResult = await withTimeout(
27393
27429
  runFormation({
27394
27430
  entityName,
27395
27431
  type,
@@ -27510,7 +27546,7 @@ ${modeIcon} Formation[active]: ${formationResult.evaluation.category} (${formati
27510
27546
  facts: d.facts,
27511
27547
  score: similarEntries[i]?.score ?? 0
27512
27548
  }));
27513
- const decision = await withTimeout2(
27549
+ const decision = await withTimeout(
27514
27550
  compactOnWrite(
27515
27551
  { title, narrative, facts: safeFacts ?? [] },
27516
27552
  existingMemories
@@ -27606,7 +27642,7 @@ Mode: ${decision.usedLLM ? "LLM" : "heuristic"}`
27606
27642
  let compressionNote = "";
27607
27643
  try {
27608
27644
  const { compressNarrative: compressNarrative2 } = await Promise.resolve().then(() => (init_quality(), quality_exports));
27609
- const { compressed, saved, usedLLM } = await withTimeout2(
27645
+ const { compressed, saved, usedLLM } = await withTimeout(
27610
27646
  compressNarrative2(narrative, safeFacts, type),
27611
27647
  COMPRESSION_TIMEOUT_MS,
27612
27648
  "Narrative compression"
@@ -27728,7 +27764,7 @@ Auto-enriched: ${enrichmentParts.join(", ")}` : "";
27728
27764
  },
27729
27765
  getEntityNames: () => graphManager.getEntityNames()
27730
27766
  };
27731
- const formed = await withTimeout2(
27767
+ const formed = await withTimeout(
27732
27768
  runFormation({ entityName, type, title, narrative, facts: safeFacts, projectId: project.id, source: "explicit", topicKey }, formationConfig),
27733
27769
  FORMATION_TIMEOUT_MS,
27734
27770
  "Shadow formation"
@@ -27819,6 +27855,9 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27819
27855
  source: z2.enum(["agent", "git", "manual"]).optional().describe(
27820
27856
  'Filter by memory source. "git" returns only commit-derived ground truth memories. Omit for all sources.'
27821
27857
  ),
27858
+ quality: z2.enum(["fast", "balanced", "thorough"]).optional().default("balanced").describe(
27859
+ "Retrieval profile: fast stays local, balanced uses configured embeddings, thorough explicitly permits optional LLM refinement."
27860
+ ),
27822
27861
  purpose: z2.string().optional().describe(
27823
27862
  "Why this must expand beyond the latest Autopilot brief. Name the missing fact or the user's explicit request."
27824
27863
  ),
@@ -27827,7 +27866,7 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27827
27866
  )
27828
27867
  }
27829
27868
  },
27830
- async ({ query, limit, type, maxTokens, scope, since, until, status, source, purpose, force }) => {
27869
+ async ({ query, limit, type, maxTokens, scope, since, until, status, source, quality, purpose, force }) => {
27831
27870
  if (scope !== "global") {
27832
27871
  const unresolved = requireResolvedProject("search the current project");
27833
27872
  if (unresolved) return unresolved;
@@ -27852,16 +27891,14 @@ Use this as the \`topicKey\` parameter in \`memorix_store\` to enable upsert beh
27852
27891
  projectId: scope === "global" ? void 0 : project.id,
27853
27892
  status: status ?? "active",
27854
27893
  source,
27894
+ quality,
27855
27895
  reader: getObservationReader(scope === "global" ? "global" : "project")
27856
27896
  });
27857
- const timeoutPromise = new Promise(
27858
- (_, reject) => setTimeout(() => reject(new Error(`Search timeout after ${TIMEOUT_MS}ms`)), TIMEOUT_MS)
27859
- );
27860
27897
  let result;
27861
27898
  try {
27862
- result = await Promise.race([searchPromise, timeoutPromise]);
27899
+ result = await withTimeout(searchPromise, TIMEOUT_MS, "Search");
27863
27900
  } catch (error) {
27864
- if (error instanceof Error && error.message.includes("timeout")) {
27901
+ if (error instanceof Error && /\btimeout\b|timed out/i.test(error.message)) {
27865
27902
  return {
27866
27903
  content: [
27867
27904
  {
@@ -31030,6 +31067,7 @@ var MemoryClient = class {
31030
31067
  type: options.type,
31031
31068
  source: options.source,
31032
31069
  status: options.status === "all" ? void 0 : options.status ?? "active",
31070
+ quality: options.quality,
31033
31071
  reader: this._reader
31034
31072
  };
31035
31073
  return this._oramaStore.searchObservations(searchOpts);