memorix 1.7.2 → 1.7.3

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 (39) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/assets/star-history-dark.svg +4 -4
  3. package/assets/star-history-light.svg +4 -4
  4. package/assets/star-history.json +47 -3
  5. package/dist/cli/index.js +396 -64
  6. package/dist/index.js +368 -62
  7. package/dist/maintenance-runner.js +365 -58
  8. package/dist/media-audio-runner.js +365 -58
  9. package/dist/media-video-runner.js +365 -58
  10. package/dist/memcode-runtime/CHANGELOG.md +27 -1
  11. package/dist/sdk.js +367 -61
  12. package/dist/vector-backfill-runner.js +365 -58
  13. package/docs/ARCHITECTURE.md +1 -1
  14. package/docs/CONFIGURATION.md +57 -1
  15. package/docs/PERFORMANCE.md +3 -1
  16. package/docs/SETUP.md +2 -1
  17. package/package.json +1 -1
  18. package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
  19. package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
  20. package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
  21. package/plugins/copilot/memorix/plugin.json +1 -1
  22. package/plugins/gemini/memorix/gemini-extension.json +1 -1
  23. package/plugins/omp/memorix/package.json +1 -1
  24. package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
  25. package/plugins/pi/memorix/package.json +1 -1
  26. package/server.json +2 -2
  27. package/src/cli/commands/config-migrate.ts +7 -0
  28. package/src/cli/commands/doctor.ts +5 -1
  29. package/src/cli/commands/init.ts +17 -0
  30. package/src/cli/commands/status.ts +2 -0
  31. package/src/config/resolved-config.ts +110 -12
  32. package/src/config/toml-loader.ts +8 -0
  33. package/src/config/yaml-loader.ts +9 -0
  34. package/src/llm/provider.ts +33 -7
  35. package/src/multimodal/image-loader.ts +6 -3
  36. package/src/rerank/http-provider.ts +125 -0
  37. package/src/rerank/index.ts +81 -0
  38. package/src/rerank/policy.ts +88 -0
  39. package/src/store/orama-store.ts +79 -53
@@ -176,6 +176,7 @@ function loadYamlConfig(projectRoot) {
176
176
  llm: { ...userConfig.llm, ...projectConfig.llm },
177
177
  agent: { ...userConfig.agent, ...projectConfig.agent },
178
178
  embedding: { ...userConfig.embedding, ...projectConfig.embedding },
179
+ rerank: { ...userConfig.rerank, ...projectConfig.rerank },
179
180
  git: { ...userConfig.git, ...projectConfig.git },
180
181
  codegraph: { ...userConfig.codegraph, ...projectConfig.codegraph },
181
182
  behavior: { ...userConfig.behavior, ...projectConfig.behavior },
@@ -1836,6 +1837,7 @@ function mergeTomlConfig(base, override) {
1836
1837
  llm: { ...base.memory?.llm, ...override.memory?.llm }
1837
1838
  },
1838
1839
  embedding: { ...base.embedding, ...override.embedding },
1840
+ rerank: { ...base.rerank, ...override.rerank },
1839
1841
  hooks: { ...base.hooks, ...override.hooks },
1840
1842
  git: { ...base.git, ...override.git },
1841
1843
  codegraph: { ...base.codegraph, ...override.codegraph },
@@ -1932,6 +1934,7 @@ __export(resolved_config_exports, {
1932
1934
  getResolvedConfigForCwd: () => getResolvedConfigForCwd,
1933
1935
  getResolvedEmbeddingLane: () => getResolvedEmbeddingLane,
1934
1936
  getResolvedMemoryLane: () => getResolvedMemoryLane,
1937
+ getResolvedRerankLane: () => getResolvedRerankLane,
1935
1938
  resetResolvedConfigCache: () => resetResolvedConfigCache
1936
1939
  });
1937
1940
  import { homedir as homedir5 } from "os";
@@ -1964,6 +1967,16 @@ function getResolvedConfig(options = {}) {
1964
1967
  legacy.llm?.baseUrl
1965
1968
  );
1966
1969
  const openRouterMemoryLlmApiKey = isOpenRouterMemoryLane(memoryLlmProvider, memoryLlmBaseUrl) ? process.env.OPENROUTER_API_KEY : void 0;
1970
+ const memoryLlmApiKey = first(
1971
+ process.env.MEMORIX_LLM_API_KEY,
1972
+ process.env.MEMORIX_API_KEY,
1973
+ toml.memory?.llm?.api_key,
1974
+ yaml.llm?.apiKey,
1975
+ legacy.llm?.apiKey,
1976
+ process.env.OPENAI_API_KEY,
1977
+ process.env.ANTHROPIC_API_KEY,
1978
+ openRouterMemoryLlmApiKey
1979
+ );
1967
1980
  const resolved = {
1968
1981
  agent: {
1969
1982
  provider: first(
@@ -2004,16 +2017,7 @@ function getResolvedConfig(options = {}) {
2004
2017
  provider: memoryLlmProvider,
2005
2018
  model: memoryLlmModel,
2006
2019
  baseUrl: memoryLlmBaseUrl,
2007
- apiKey: first(
2008
- process.env.MEMORIX_LLM_API_KEY,
2009
- process.env.MEMORIX_API_KEY,
2010
- toml.memory?.llm?.api_key,
2011
- yaml.llm?.apiKey,
2012
- legacy.llm?.apiKey,
2013
- process.env.OPENAI_API_KEY,
2014
- process.env.ANTHROPIC_API_KEY,
2015
- openRouterMemoryLlmApiKey
2016
- )
2020
+ apiKey: memoryLlmApiKey
2017
2021
  }
2018
2022
  },
2019
2023
  embedding: {
@@ -2023,6 +2027,12 @@ function getResolvedConfig(options = {}) {
2023
2027
  apiKey: first(process.env.MEMORIX_EMBEDDING_API_KEY, toml.embedding?.api_key, yaml.embedding?.apiKey, legacy.embeddingApi?.apiKey, openRouterEmbeddingApiKey),
2024
2028
  dimensions: firstNumber(parseNumber(process.env.MEMORIX_EMBEDDING_DIMENSIONS), toml.embedding?.dimensions, yaml.embedding?.dimensions, legacy.embeddingApi?.dimensions)
2025
2029
  },
2030
+ rerank: resolveRerankLane({
2031
+ toml,
2032
+ yaml,
2033
+ memoryLlmApiKey,
2034
+ memoryLlmBaseUrl
2035
+ }),
2026
2036
  git: {
2027
2037
  autoHook: firstBool(toml.git?.auto_hook, yaml.git?.autoHook),
2028
2038
  ingestOnCommit: firstBool(toml.git?.ingest_on_commit, yaml.git?.ingestOnCommit),
@@ -2092,6 +2102,9 @@ function getResolvedMemoryLane(options = {}) {
2092
2102
  function getResolvedEmbeddingLane(options = {}) {
2093
2103
  return getResolvedConfig(options).embedding;
2094
2104
  }
2105
+ function getResolvedRerankLane(options = {}) {
2106
+ return getResolvedConfig(options).rerank;
2107
+ }
2095
2108
  function resetResolvedConfigCache() {
2096
2109
  }
2097
2110
  function first(...values) {
@@ -2134,6 +2147,10 @@ function getEnvSourceNames() {
2134
2147
  "MEMORIX_EMBEDDING_BASE_URL",
2135
2148
  "MEMORIX_EMBEDDING_MODEL",
2136
2149
  "MEMORIX_EMBEDDING_DIMENSIONS",
2150
+ "MEMORIX_RERANK_PROVIDER",
2151
+ "MEMORIX_RERANK_MODEL",
2152
+ "MEMORIX_RERANK_BASE_URL",
2153
+ "MEMORIX_RERANK_API_KEY",
2137
2154
  "MEMORIX_CODEGRAPH_EXTERNAL_CONTEXT",
2138
2155
  "MEMORIX_CODEGRAPH_EXTERNAL_COMMAND",
2139
2156
  "MEMORIX_CODEGRAPH_EXTERNAL_TIMEOUT_MS",
@@ -2157,6 +2174,63 @@ function normalizeExternalContext(value) {
2157
2174
  if (normalized === "auto" || normalized === "off") return normalized;
2158
2175
  return void 0;
2159
2176
  }
2177
+ function normalizeRerankProvider(value) {
2178
+ const normalized = value?.trim().toLowerCase();
2179
+ if (normalized === "http") return "http";
2180
+ return "off";
2181
+ }
2182
+ function resolveRerankLane(args) {
2183
+ const provider2 = normalizeRerankProvider(first(
2184
+ process.env.MEMORIX_RERANK_PROVIDER,
2185
+ args.toml.rerank?.provider,
2186
+ args.yaml.rerank?.provider,
2187
+ "off"
2188
+ ));
2189
+ const model = first(
2190
+ process.env.MEMORIX_RERANK_MODEL,
2191
+ args.toml.rerank?.model,
2192
+ args.yaml.rerank?.model
2193
+ );
2194
+ const explicitBaseUrl = first(
2195
+ process.env.MEMORIX_RERANK_BASE_URL,
2196
+ args.toml.rerank?.base_url,
2197
+ args.yaml.rerank?.baseUrl
2198
+ );
2199
+ const baseUrl = provider2 === "http" ? first(explicitBaseUrl, args.memoryLlmBaseUrl) : explicitBaseUrl;
2200
+ const dedicatedApiKey = first(
2201
+ process.env.MEMORIX_RERANK_API_KEY,
2202
+ args.toml.rerank?.api_key,
2203
+ args.yaml.rerank?.apiKey
2204
+ );
2205
+ const sameEndpoint = provider2 === "http" && sameTrustedEndpoint(baseUrl, args.memoryLlmBaseUrl);
2206
+ const inheritedApiKey = sameEndpoint ? args.memoryLlmApiKey : void 0;
2207
+ const apiKey = first(dedicatedApiKey, inheritedApiKey);
2208
+ const canSendRequest = provider2 === "http" && Boolean(baseUrl) && (Boolean(apiKey) || !args.memoryLlmApiKey || sameEndpoint);
2209
+ return { provider: provider2, model, baseUrl, apiKey, canSendRequest };
2210
+ }
2211
+ function sameTrustedEndpoint(left, right) {
2212
+ const a = normalizeTrustedEndpoint(left);
2213
+ const b = normalizeTrustedEndpoint(right);
2214
+ return Boolean(a && b && a === b);
2215
+ }
2216
+ function normalizeTrustedEndpoint(value) {
2217
+ const trimmed = value?.trim();
2218
+ if (!trimmed) return void 0;
2219
+ try {
2220
+ const url = new URL(trimmed);
2221
+ const protocol = url.protocol.toLowerCase();
2222
+ const hostname = url.hostname.toLowerCase();
2223
+ const port = url.port;
2224
+ let pathname = url.pathname.replace(/\/+$/, "");
2225
+ if (pathname.toLowerCase().endsWith("/rerank")) {
2226
+ pathname = pathname.slice(0, -"/rerank".length).replace(/\/+$/, "");
2227
+ }
2228
+ return `${protocol}//${hostname}${port ? `:${port}` : ""}${pathname}`;
2229
+ } catch {
2230
+ const fallback = trimmed.replace(/\/+$/, "").replace(/\/rerank$/i, "").replace(/\/+$/, "");
2231
+ return fallback.toLowerCase() || void 0;
2232
+ }
2233
+ }
2160
2234
  var init_resolved_config = __esm({
2161
2235
  "src/config/resolved-config.ts"() {
2162
2236
  "use strict";
@@ -4392,9 +4466,27 @@ __export(provider_exports2, {
4392
4466
  getLLMConfig: () => getLLMConfig,
4393
4467
  initLLM: () => initLLM,
4394
4468
  isLLMEnabled: () => isLLMEnabled,
4469
+ normalizeOpenAICompatibleBaseUrl: () => normalizeOpenAICompatibleBaseUrl,
4395
4470
  parseLLMTimeoutMs: () => parseLLMTimeoutMs,
4396
4471
  setLLMConfig: () => setLLMConfig
4397
4472
  });
4473
+ function normalizeOpenAICompatibleBaseUrl(baseUrl) {
4474
+ const base = baseUrl.trim().replace(/\/+$/, "");
4475
+ if (!base) return base;
4476
+ let pathname = "";
4477
+ try {
4478
+ pathname = new URL(base).pathname.replace(/\/+$/, "");
4479
+ } catch {
4480
+ pathname = base.split(/[?#]/, 1)[0].replace(/\/+$/, "");
4481
+ }
4482
+ const lastSegment = pathname.split("/").filter(Boolean).at(-1);
4483
+ if (lastSegment && OPENAI_API_VERSION_SEGMENT.test(lastSegment)) {
4484
+ return base;
4485
+ }
4486
+ const queryOrHash = base.match(/[?#].*$/)?.[0] ?? "";
4487
+ const pathWithoutQuery = base.slice(0, base.length - queryOrHash.length).replace(/\/+$/, "");
4488
+ return `${pathWithoutQuery}/v1${queryOrHash}`;
4489
+ }
4398
4490
  function parseLLMTimeoutMs(raw) {
4399
4491
  if (raw === void 0 || raw.trim() === "") return LLM_TIMEOUT_DEFAULT_MS;
4400
4492
  const parsed = Number(raw);
@@ -4498,8 +4590,7 @@ async function callLLM(systemPrompt, userMessage, signal) {
4498
4590
  }
4499
4591
  async function callOpenAICompatible(systemPrompt, userMessage, signal) {
4500
4592
  const config = currentConfig;
4501
- let base = config.baseUrl.replace(/\/+$/, "");
4502
- if (!base.endsWith("/v1")) base += "/v1";
4593
+ const base = normalizeOpenAICompatibleBaseUrl(config.baseUrl);
4503
4594
  const url = `${base}/chat/completions`;
4504
4595
  const fetchSignal = signal ? AbortSignal.any([signal, AbortSignal.timeout(LLM_CALL_TIMEOUT_MS)]) : AbortSignal.timeout(LLM_CALL_TIMEOUT_MS);
4505
4596
  const response = await fetch(url, {
@@ -4578,8 +4669,7 @@ async function callLLMWithTools(messages, tools, signal) {
4578
4669
  }
4579
4670
  async function callOpenAIWithTools(messages, tools, signal) {
4580
4671
  const config = currentConfig;
4581
- let base = config.baseUrl.replace(/\/+$/, "");
4582
- if (!base.endsWith("/v1")) base += "/v1";
4672
+ const base = normalizeOpenAICompatibleBaseUrl(config.baseUrl);
4583
4673
  const url = `${base}/chat/completions`;
4584
4674
  const openaiMessages = messages.map((msg) => {
4585
4675
  if (msg.role === "tool") {
@@ -4729,8 +4819,7 @@ async function callAnthropicWithTools(messages, tools, signal) {
4729
4819
  }
4730
4820
  async function* callOpenAIWithToolsStream(messages, tools) {
4731
4821
  const config = currentConfig;
4732
- let base = config.baseUrl.replace(/\/+$/, "");
4733
- if (!base.endsWith("/v1")) base += "/v1";
4822
+ const base = normalizeOpenAICompatibleBaseUrl(config.baseUrl);
4734
4823
  const url = `${base}/chat/completions`;
4735
4824
  const openaiMessages = messages.map((msg) => {
4736
4825
  if (msg.role === "tool") {
@@ -4978,7 +5067,7 @@ async function* callAnthropicWithToolsStream(messages, tools) {
4978
5067
  }
4979
5068
  };
4980
5069
  }
4981
- var LLM_TIMEOUT_DEFAULT_MS, LLM_TIMEOUT_MIN_MS, LLM_TIMEOUT_MAX_MS, MAX_LLM_RESPONSE_BYTES, LLM_CALL_TIMEOUT_MS, PROVIDER_DEFAULTS, currentConfig;
5070
+ var LLM_TIMEOUT_DEFAULT_MS, LLM_TIMEOUT_MIN_MS, LLM_TIMEOUT_MAX_MS, MAX_LLM_RESPONSE_BYTES, OPENAI_API_VERSION_SEGMENT, LLM_CALL_TIMEOUT_MS, PROVIDER_DEFAULTS, currentConfig;
4982
5071
  var init_provider2 = __esm({
4983
5072
  "src/llm/provider.ts"() {
4984
5073
  "use strict";
@@ -4988,6 +5077,7 @@ var init_provider2 = __esm({
4988
5077
  LLM_TIMEOUT_MIN_MS = 1e3;
4989
5078
  LLM_TIMEOUT_MAX_MS = 3e5;
4990
5079
  MAX_LLM_RESPONSE_BYTES = 2 * 1024 * 1024;
5080
+ OPENAI_API_VERSION_SEGMENT = /^v\d+(?:[a-z][a-z0-9-]*)?$/i;
4991
5081
  LLM_CALL_TIMEOUT_MS = parseLLMTimeoutMs(process.env.MEMORIX_LLM_TIMEOUT_MS);
4992
5082
  PROVIDER_DEFAULTS = {
4993
5083
  openai: { baseUrl: "https://api.openai.com/v1", model: "gpt-4.1-nano" },
@@ -5047,6 +5137,48 @@ Rules:
5047
5137
  }
5048
5138
  });
5049
5139
 
5140
+ // src/rerank/policy.ts
5141
+ function parseRerankTimeoutMs(raw) {
5142
+ const value = raw?.trim();
5143
+ if (!value) return DEFAULT_RERANK_TIMEOUT_MS;
5144
+ const parsed = Number(value);
5145
+ if (!Number.isInteger(parsed) || Number.isNaN(parsed)) {
5146
+ console.warn(
5147
+ `[memorix] MEMORIX_RERANK_TIMEOUT_MS="${raw}" is invalid (must be a positive integer between ${RERANK_TIMEOUT_MIN_MS}-${RERANK_TIMEOUT_MAX_MS}ms). Using default ${DEFAULT_RERANK_TIMEOUT_MS}ms.`
5148
+ );
5149
+ return DEFAULT_RERANK_TIMEOUT_MS;
5150
+ }
5151
+ if (parsed < RERANK_TIMEOUT_MIN_MS) return RERANK_TIMEOUT_MIN_MS;
5152
+ if (parsed > RERANK_TIMEOUT_MAX_MS) return RERANK_TIMEOUT_MAX_MS;
5153
+ return parsed;
5154
+ }
5155
+ function resolveRerankTimeoutMs(explicitTimeoutMs) {
5156
+ if (typeof explicitTimeoutMs === "number" && Number.isFinite(explicitTimeoutMs) && explicitTimeoutMs > 0) {
5157
+ return explicitTimeoutMs;
5158
+ }
5159
+ return parseRerankTimeoutMs(process.env.MEMORIX_RERANK_TIMEOUT_MS);
5160
+ }
5161
+ function shouldAttemptHttpRerank(input) {
5162
+ return input.quality === "thorough" && input.hasQuery && input.candidateCount >= MIN_RERANK_CANDIDATES;
5163
+ }
5164
+ function shouldAttemptLlmRerank(input) {
5165
+ if (input.httpRerankConfigured) return false;
5166
+ if (input.quality !== "thorough" || input.tier !== "heavy" || !input.hasQuery) return false;
5167
+ if (input.candidateCount < MIN_RERANK_CANDIDATES) return false;
5168
+ return input.topScore > 0 && input.secondScore / input.topScore > 0.7;
5169
+ }
5170
+ var DEFAULT_RERANK_TIMEOUT_MS, RERANK_TIMEOUT_MIN_MS, RERANK_TIMEOUT_MAX_MS, MIN_RERANK_CANDIDATES;
5171
+ var init_policy = __esm({
5172
+ "src/rerank/policy.ts"() {
5173
+ "use strict";
5174
+ init_esm_shims();
5175
+ DEFAULT_RERANK_TIMEOUT_MS = 3e4;
5176
+ RERANK_TIMEOUT_MIN_MS = 1e3;
5177
+ RERANK_TIMEOUT_MAX_MS = 3e5;
5178
+ MIN_RERANK_CANDIDATES = 3;
5179
+ }
5180
+ });
5181
+
5050
5182
  // src/timeout.ts
5051
5183
  function withTimeout(promise, ms, label) {
5052
5184
  return new Promise((resolve, reject) => {
@@ -5332,6 +5464,148 @@ var init_aliases = __esm({
5332
5464
  }
5333
5465
  });
5334
5466
 
5467
+ // src/rerank/http-provider.ts
5468
+ function buildRerankUrl(baseUrl) {
5469
+ const trimmed = baseUrl.trim().replace(/\/+$/, "");
5470
+ if (trimmed.endsWith("/rerank")) return trimmed;
5471
+ return `${trimmed}/rerank`;
5472
+ }
5473
+ function parseRerankResponse(payload) {
5474
+ const rows = extractRows(payload);
5475
+ if (rows.length === 0) {
5476
+ throw new Error("Neural rerank response contained no ranked results");
5477
+ }
5478
+ return rows.sort((a, b) => b.score - a.score);
5479
+ }
5480
+ function extractRows(payload) {
5481
+ if (Array.isArray(payload)) {
5482
+ return payload.map(rowFromUnknown).filter((row) => row !== null);
5483
+ }
5484
+ if (payload && typeof payload === "object" && "results" in payload) {
5485
+ const results = payload.results;
5486
+ if (Array.isArray(results)) {
5487
+ return results.map(rowFromUnknown).filter((row) => row !== null);
5488
+ }
5489
+ }
5490
+ throw new Error("Neural rerank response was not Cohere or TEI shaped");
5491
+ }
5492
+ function rowFromUnknown(value) {
5493
+ if (!value || typeof value !== "object") return null;
5494
+ const row = value;
5495
+ if (typeof row.index !== "number" || !Number.isFinite(row.index)) return null;
5496
+ const score = typeof row.relevance_score === "number" ? row.relevance_score : typeof row.score === "number" ? row.score : null;
5497
+ if (score === null || !Number.isFinite(score)) return null;
5498
+ return { index: row.index, score };
5499
+ }
5500
+ function buildRerankBody(query, documents, model) {
5501
+ const body = {
5502
+ query,
5503
+ documents,
5504
+ top_n: documents.length,
5505
+ return_documents: false
5506
+ };
5507
+ const trimmed = model?.trim();
5508
+ if (trimmed) body.model = trimmed;
5509
+ return body;
5510
+ }
5511
+ async function rerankViaHttp(query, documents, config) {
5512
+ if (documents.length === 0) return [];
5513
+ const url = buildRerankUrl(config.baseUrl);
5514
+ const headers = {
5515
+ "content-type": "application/json"
5516
+ };
5517
+ if (config.apiKey) {
5518
+ headers.authorization = `Bearer ${config.apiKey}`;
5519
+ }
5520
+ const controller = new AbortController();
5521
+ const timeoutMs = resolveRerankTimeoutMs(config.timeoutMs);
5522
+ const timer = setTimeout(() => controller.abort(), timeoutMs);
5523
+ try {
5524
+ const fetchImpl = config.fetchImpl ?? fetch;
5525
+ const response = await fetchImpl(url, {
5526
+ method: "POST",
5527
+ headers,
5528
+ signal: controller.signal,
5529
+ body: JSON.stringify(buildRerankBody(query, documents, config.model))
5530
+ });
5531
+ if (!response.ok) {
5532
+ throw new Error(`Neural rerank HTTP ${response.status} from ${url}`);
5533
+ }
5534
+ return parseRerankResponse(await response.json());
5535
+ } finally {
5536
+ clearTimeout(timer);
5537
+ }
5538
+ }
5539
+ var init_http_provider = __esm({
5540
+ "src/rerank/http-provider.ts"() {
5541
+ "use strict";
5542
+ init_esm_shims();
5543
+ init_policy();
5544
+ }
5545
+ });
5546
+
5547
+ // src/rerank/index.ts
5548
+ var rerank_exports = {};
5549
+ __export(rerank_exports, {
5550
+ DEFAULT_RERANK_TIMEOUT_MS: () => DEFAULT_RERANK_TIMEOUT_MS,
5551
+ MIN_RERANK_CANDIDATES: () => MIN_RERANK_CANDIDATES,
5552
+ isNeuralRerankEnabled: () => isNeuralRerankEnabled,
5553
+ neuralRerankCandidates: () => neuralRerankCandidates,
5554
+ parseRerankTimeoutMs: () => parseRerankTimeoutMs,
5555
+ resolveRerankTimeoutMs: () => resolveRerankTimeoutMs,
5556
+ shouldAttemptHttpRerank: () => shouldAttemptHttpRerank,
5557
+ shouldAttemptLlmRerank: () => shouldAttemptLlmRerank
5558
+ });
5559
+ function isNeuralRerankEnabled(options = {}) {
5560
+ const lane = getResolvedRerankLane(options);
5561
+ return lane.provider === "http" && Boolean(lane.baseUrl);
5562
+ }
5563
+ async function neuralRerankCandidates(query, candidates, options = {}) {
5564
+ if (!isNeuralRerankEnabled(options) || candidates.length === 0) return null;
5565
+ const lane = getResolvedRerankLane(options);
5566
+ if (!lane.canSendRequest || !lane.baseUrl) return null;
5567
+ const toRerank = candidates.slice(0, MAX_RERANK);
5568
+ const rest = candidates.slice(MAX_RERANK);
5569
+ const documents = toRerank.map((candidate) => candidateText(candidate));
5570
+ const ranked = await rerankViaHttp(query, documents, {
5571
+ provider: "http",
5572
+ model: lane.model,
5573
+ baseUrl: lane.baseUrl,
5574
+ apiKey: lane.apiKey,
5575
+ timeoutMs: resolveRerankTimeoutMs()
5576
+ });
5577
+ const seen = /* @__PURE__ */ new Set();
5578
+ const reranked = [];
5579
+ for (const row of ranked) {
5580
+ const candidate = toRerank[row.index];
5581
+ if (!candidate || seen.has(row.index)) continue;
5582
+ reranked.push(candidate);
5583
+ seen.add(row.index);
5584
+ }
5585
+ for (const [index, candidate] of toRerank.entries()) {
5586
+ if (!seen.has(index)) reranked.push(candidate);
5587
+ }
5588
+ reranked.push(...rest);
5589
+ return reranked;
5590
+ }
5591
+ function candidateText(candidate) {
5592
+ const narrative = candidate.narrative?.trim();
5593
+ if (narrative) return `${candidate.title} \u2014 ${narrative.slice(0, 200)}`;
5594
+ return candidate.title;
5595
+ }
5596
+ var MAX_RERANK;
5597
+ var init_rerank = __esm({
5598
+ "src/rerank/index.ts"() {
5599
+ "use strict";
5600
+ init_esm_shims();
5601
+ init_resolved_config();
5602
+ init_http_provider();
5603
+ init_policy();
5604
+ init_policy();
5605
+ MAX_RERANK = 10;
5606
+ }
5607
+ });
5608
+
5335
5609
  // src/llm/quality.ts
5336
5610
  var quality_exports = {};
5337
5611
  __export(quality_exports, {
@@ -5371,9 +5645,9 @@ async function rerankResults(query, candidates) {
5371
5645
  if (!isLLMEnabled() || candidates.length <= 2) {
5372
5646
  return { reranked: candidates, usedLLM: false };
5373
5647
  }
5374
- const MAX_RERANK = 10;
5375
- const toRerank = candidates.slice(0, MAX_RERANK);
5376
- const rest = candidates.slice(MAX_RERANK);
5648
+ const MAX_RERANK2 = 10;
5649
+ const toRerank = candidates.slice(0, MAX_RERANK2);
5650
+ const rest = candidates.slice(MAX_RERANK2);
5377
5651
  try {
5378
5652
  const candidateList = toRerank.map(
5379
5653
  (c) => `[ID: ${c.id}] (${c.type}) ${c.title}${c.narrative ? ` \u2014 ${c.narrative.substring(0, 100)}` : ""}`
@@ -6266,46 +6540,78 @@ async function searchObservations(options) {
6266
6540
  }
6267
6541
  }
6268
6542
  }
6269
- const shouldRerank = quality === "thorough" && tier === "heavy" && hasQuery && intermediate.length > 2 && (() => {
6270
- const top = intermediate[0]?.score ?? 0;
6271
- const second = intermediate[1]?.score ?? 0;
6272
- return top > 0 && second / top > 0.7;
6273
- })();
6274
- if (shouldRerank) {
6275
- try {
6276
- const { rerankResults: rerankResults2 } = await Promise.resolve().then(() => (init_quality(), quality_exports));
6277
- const narrativeMap = /* @__PURE__ */ new Map();
6278
- for (const hit of results.hits) {
6279
- const doc = hit.document;
6280
- narrativeMap.set(makeEntryKey(doc.projectId, doc.observationId), doc.narrative);
6281
- }
6282
- const RERANK_TOP_K = 5;
6283
- const toRerank = intermediate.slice(0, RERANK_TOP_K);
6284
- const candidates = toRerank.map((e2, index) => ({
6285
- id: `r${index + 1}`,
6286
- title: e2.title,
6287
- type: e2.type,
6288
- score: e2.score,
6289
- narrative: narrativeMap.get(makeEntryKey(e2.projectId, e2.id))
6290
- }));
6291
- const _parsedRerank = parseInt(process.env.MEMORIX_RERANK_TIMEOUT_MS || "", 10);
6292
- const RERANK_TIMEOUT_MS = Number.isFinite(_parsedRerank) && _parsedRerank > 0 ? _parsedRerank : 5e3;
6293
- const { reranked, usedLLM } = await withTimeout(
6294
- rerankResults2(originalQuery, candidates),
6295
- RERANK_TIMEOUT_MS,
6296
- "LLM rerank"
6297
- );
6298
- mark(`rerank(usedLLM=${usedLLM})`);
6299
- if (usedLLM) {
6300
- lastSearchModeByProject.set(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + LLM rerank");
6301
- const candidateMap = new Map(candidates.map((candidate, index) => [candidate.id, toRerank[index]]));
6302
- const rerankedTop = reranked.map((r2) => candidateMap.get(r2.id)).filter((e2) => e2 != null);
6303
- if (rerankedTop.length > 0) {
6304
- intermediate = [...rerankedTop, ...intermediate.slice(RERANK_TOP_K)];
6543
+ const httpGate = shouldAttemptHttpRerank({
6544
+ quality,
6545
+ hasQuery: Boolean(hasQuery),
6546
+ candidateCount: intermediate.length
6547
+ });
6548
+ const llmGateWithoutHttp = shouldAttemptLlmRerank({
6549
+ quality,
6550
+ tier,
6551
+ hasQuery: Boolean(hasQuery),
6552
+ candidateCount: intermediate.length,
6553
+ topScore: intermediate[0]?.score ?? 0,
6554
+ secondScore: intermediate[1]?.score ?? 0,
6555
+ httpRerankConfigured: false
6556
+ });
6557
+ if (httpGate || llmGateWithoutHttp) {
6558
+ const narrativeMap = /* @__PURE__ */ new Map();
6559
+ for (const hit of results.hits) {
6560
+ const doc = hit.document;
6561
+ narrativeMap.set(makeEntryKey(doc.projectId, doc.observationId), doc.narrative);
6562
+ }
6563
+ const RERANK_TOP_K = 5;
6564
+ const toRerank = intermediate.slice(0, RERANK_TOP_K);
6565
+ const candidates = toRerank.map((e2, index) => ({
6566
+ id: `r${index + 1}`,
6567
+ title: e2.title,
6568
+ type: e2.type,
6569
+ score: e2.score,
6570
+ narrative: narrativeMap.get(makeEntryKey(e2.projectId, e2.id))
6571
+ }));
6572
+ const RERANK_TIMEOUT_MS = parseRerankTimeoutMs(process.env.MEMORIX_RERANK_TIMEOUT_MS);
6573
+ const candidateMap = new Map(candidates.map((candidate, index) => [candidate.id, toRerank[index]]));
6574
+ const { isNeuralRerankEnabled: isNeuralRerankEnabled2, neuralRerankCandidates: neuralRerankCandidates2 } = await Promise.resolve().then(() => (init_rerank(), rerank_exports));
6575
+ const httpConfigured = isNeuralRerankEnabled2();
6576
+ if (httpConfigured && httpGate) {
6577
+ try {
6578
+ const neural = await withTimeout(
6579
+ neuralRerankCandidates2(originalQuery, candidates),
6580
+ RERANK_TIMEOUT_MS,
6581
+ "neural rerank"
6582
+ );
6583
+ if (neural && neural.length > 0) {
6584
+ const rerankedTop = neural.map((row) => candidateMap.get(row.id)).filter((entry) => entry != null);
6585
+ if (rerankedTop.length > 0) {
6586
+ intermediate = [...rerankedTop, ...intermediate.slice(RERANK_TOP_K)];
6587
+ lastSearchModeByProject.set(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + neural rerank");
6588
+ mark("rerank(usedNeural=true)");
6589
+ }
6305
6590
  }
6591
+ } catch {
6592
+ console.error("[memorix] neural rerank failed or timed out; keeping original order");
6306
6593
  }
6307
- } catch (error) {
6308
- console.error("[memorix] LLM rerank failed or timed out, using original order");
6594
+ } else if (!httpConfigured && llmGateWithoutHttp) {
6595
+ try {
6596
+ const { rerankResults: rerankResults2 } = await Promise.resolve().then(() => (init_quality(), quality_exports));
6597
+ const { reranked, usedLLM } = await withTimeout(
6598
+ rerankResults2(originalQuery, candidates),
6599
+ RERANK_TIMEOUT_MS,
6600
+ "LLM rerank"
6601
+ );
6602
+ mark(`rerank(usedLLM=${usedLLM})`);
6603
+ if (usedLLM) {
6604
+ lastSearchModeByProject.set(modeKey, (lastSearchModeByProject.get(modeKey) ?? "fulltext") + " + LLM rerank");
6605
+ const rerankedTop = reranked.map((row) => candidateMap.get(row.id)).filter((entry) => entry != null);
6606
+ if (rerankedTop.length > 0) {
6607
+ intermediate = [...rerankedTop, ...intermediate.slice(RERANK_TOP_K)];
6608
+ }
6609
+ }
6610
+ } catch {
6611
+ console.error("[memorix] LLM rerank failed or timed out, using original order");
6612
+ }
6613
+ } else {
6614
+ mark(`rerank(skipped,tier=${tier})`);
6309
6615
  }
6310
6616
  } else {
6311
6617
  mark(`rerank(skipped,tier=${tier})`);
@@ -6486,6 +6792,7 @@ var init_orama_store = __esm({
6486
6792
  init_project_affinity();
6487
6793
  init_intent_detector();
6488
6794
  init_query_expansion();
6795
+ init_policy();
6489
6796
  init_timeout();
6490
6797
  db = null;
6491
6798
  dbInitPromise = null;
@@ -391,7 +391,7 @@ Resolution order:
391
391
  2. project `.env`
392
392
  3. user `~/.memorix/.env`
393
393
 
394
- The primary lanes are `[agent]` for memcode, `[memory.llm]` for formation/rerank/summaries, and `[embedding]` for semantic search. The dashboard and `memorix status` expose config provenance so the active value source is visible.
394
+ The primary lanes are `[agent]` for memcode, `[memory.llm]` for formation/summaries/LLM-rerank fallback, `[rerank]` for optional HTTP rerank against a compatible `/rerank` API, and `[embedding]` for semantic search. The dashboard and `memorix status` expose config provenance so the active value source is visible.
395
395
 
396
396
  ---
397
397
 
@@ -47,6 +47,12 @@ model = "text-embedding-v4"
47
47
  base_url = "https://dashscope.aliyuncs.com/compatible-mode/v1"
48
48
  api_key = "..."
49
49
 
50
+ # Optional HTTP rerank. Off unless provider = "http".
51
+ # [rerank]
52
+ # provider = "http"
53
+ # model = "rerank-model"
54
+ # base_url = "https://api.example.com/v1"
55
+
50
56
  [memory]
51
57
  inject = "minimal"
52
58
  formation = "active"
@@ -127,7 +133,7 @@ Used by Memorix background memory intelligence:
127
133
  - memory formation
128
134
  - summarization
129
135
  - deduplication
130
- - optional reranking
136
+ - optional LLM reranking fallback
131
137
  - cleanup assistance
132
138
 
133
139
  Common keys:
@@ -139,6 +145,9 @@ Common keys:
139
145
 
140
146
  For OpenAI-compatible providers such as DashScope, DeepSeek-compatible gateways,
141
147
  or internal model gateways, use `provider = "openai"` and set `base_url`.
148
+ Memorix uses an explicit provider path as-is: for example,
149
+ `https://ark.cn-beijing.volces.com/api/v3` remains `/api/v3`. Only a bare host
150
+ without a versioned path receives the convenience suffix `/v1`.
142
151
 
143
152
  ### `[embedding]`
144
153
 
@@ -186,6 +195,53 @@ OpenAI-only default. The equivalent env-var form is
186
195
  `MEMORIX_EMBEDDING=api`, `MEMORIX_EMBEDDING_BASE_URL=https://openrouter.ai/api/v1`,
187
196
  `MEMORIX_EMBEDDING_MODEL=qwen/qwen3-embedding-8b`.
188
197
 
198
+ ### `[rerank]`
199
+
200
+ Optional HTTP rerank for thorough search. Off by default.
201
+ When `provider = "http"`, Memorix POSTs a Cohere-compatible
202
+ `{model, query, documents}` body to `{base_url}/rerank` (or an explicit
203
+ `/rerank` path). Any compatible endpoint works — hosted APIs, TEI, vLLM,
204
+ or a local gateway. Memorix sends the configured `model` string unchanged.
205
+
206
+ Common keys:
207
+
208
+ - `provider` — `off` (default) or `http`
209
+ - `model` — user-configured model id (no built-in default). Example:
210
+ `rerank-model`
211
+ - `base_url` — API root that serves `/rerank`, for example
212
+ `https://api.example.com/v1` or `http://127.0.0.1:8080`. When unset and
213
+ `provider = "http"`, Memorix inherits `[memory.llm].base_url`.
214
+ - `api_key` — bearer token. When unset, inherits the memory LLM key
215
+ (`MEMORIX_LLM_API_KEY`, `MEMORIX_API_KEY`, or `[memory.llm].api_key`)
216
+ only if the effective `base_url` is the same trusted endpoint as
217
+ `[memory.llm].base_url`. A different rerank URL requires
218
+ `MEMORIX_RERANK_API_KEY` or `rerank.api_key`.
219
+
220
+ Environment overrides (highest priority after CLI flags):
221
+
222
+ ```bash
223
+ MEMORIX_RERANK_PROVIDER=http
224
+ MEMORIX_RERANK_MODEL=rerank-model
225
+ MEMORIX_RERANK_BASE_URL=https://api.example.com/v1
226
+ # MEMORIX_RERANK_API_KEY= # required when base_url is not the memory LLM endpoint
227
+ ```
228
+
229
+ If the memory LLM lane already points at the same gateway, you can enable
230
+ rerank with:
231
+
232
+ ```toml
233
+ [rerank]
234
+ provider = "http"
235
+ model = "rerank-model"
236
+ ```
237
+
238
+ or set `MEMORIX_RERANK_PROVIDER=http` and `MEMORIX_RERANK_MODEL=...`.
239
+ Timeout is `MEMORIX_RERANK_TIMEOUT_MS` (default 30000). Thorough search
240
+ with at least 3 candidates uses HTTP rerank when configured. On HTTP miss
241
+ or timeout, Memorix keeps the original order and does not fall through to
242
+ LLM rerank. LLM rerank remains available when HTTP rerank is off, on the
243
+ existing thorough/heavy/ambiguous gate.
244
+
189
245
  Image analysis (visual description for ingested images) runs on the LLM lane
190
246
  through an OpenAI-compatible vision endpoint, so it can also use OpenRouter
191
247
  with a multimodal model — for example `qwen/qwen3-vl-8b-instruct` via
@@ -75,7 +75,9 @@ On the release development machine used for this check, the healthy HTTP service
75
75
  | `MEMORIX_FORMATION_TIMEOUT_MS` | `12000` (12 s) | Raise when LLM-backed formation should outlive slow proxy/provider hops |
76
76
  | `MEMORIX_LLM_API_KEY` / `OPENAI_API_KEY` | unset | Enable LLM-backed enrichment, extraction, rerank, or skill generation |
77
77
  | `MEMORIX_LLM_TIMEOUT_MS` | `30000` (30 s) | Bound a single LLM-backed extraction/resolve call |
78
- | `MEMORIX_RERANK_TIMEOUT_MS` | provider default | Bound slow LLM rerank calls |
78
+ | `MEMORIX_RERANK_TIMEOUT_MS` | 30000 | Bound HTTP and LLM rerank calls |
79
+ | `MEMORIX_RERANK_PROVIDER` | `off` | Set `http` to enable optional HTTP rerank |
80
+ | `MEMORIX_RERANK_BASE_URL` | `[memory.llm].base_url` | Compatible `/rerank` API root (path `/rerank` is appended) |
79
81
  | `memorix memory search --quality fast` | n/a | Force a fully local retrieval path for a latency-sensitive call |
80
82
  | `npm run benchmark:retrieval -- --records 1000 --runs 100` | n/a | Reproduce hot in-process lexical retrieval latency; not an end-to-end claim |
81
83
  | `npm run gate:large-store -- --records 40000` | n/a | Exercise SDK, HTTP MCP, hook persistence, cache integrity, and reopen behavior against a large isolated store |
package/docs/SETUP.md CHANGED
@@ -498,7 +498,8 @@ What each lane does:
498
498
  | Lane | Used by |
499
499
  | --- | --- |
500
500
  | `[agent]` | model used by memcode while coding |
501
- | `[memory.llm]` | memory formation, summaries, deduplication, optional rerank |
501
+ | `[memory.llm]` | memory formation, summaries, deduplication, optional LLM rerank fallback |
502
+ | `[rerank]` | optional HTTP rerank against a compatible `/rerank` API |
502
503
  | `[embedding]` | semantic/vector search |
503
504
  | `[memory]` | memory injection and formation behavior |
504
505
  | `[git]` | Git Memory hook and ingestion behavior |