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.
- package/CHANGELOG.md +27 -1
- package/assets/star-history-dark.svg +4 -4
- package/assets/star-history-light.svg +4 -4
- package/assets/star-history.json +47 -3
- package/dist/cli/index.js +396 -64
- package/dist/index.js +368 -62
- package/dist/maintenance-runner.js +365 -58
- package/dist/media-audio-runner.js +365 -58
- package/dist/media-video-runner.js +365 -58
- package/dist/memcode-runtime/CHANGELOG.md +27 -1
- package/dist/sdk.js +367 -61
- package/dist/vector-backfill-runner.js +365 -58
- package/docs/ARCHITECTURE.md +1 -1
- package/docs/CONFIGURATION.md +57 -1
- package/docs/PERFORMANCE.md +3 -1
- package/docs/SETUP.md +2 -1
- package/package.json +1 -1
- package/plugins/claude/memorix/.claude-plugin/plugin.json +1 -1
- package/plugins/codebuddy/memorix/.codebuddy-plugin/plugin.json +1 -1
- package/plugins/codex/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/copilot/memorix/plugin.json +1 -1
- package/plugins/gemini/memorix/gemini-extension.json +1 -1
- package/plugins/omp/memorix/package.json +1 -1
- package/plugins/openclaw/memorix/.codex-plugin/plugin.json +1 -1
- package/plugins/pi/memorix/package.json +1 -1
- package/server.json +2 -2
- package/src/cli/commands/config-migrate.ts +7 -0
- package/src/cli/commands/doctor.ts +5 -1
- package/src/cli/commands/init.ts +17 -0
- package/src/cli/commands/status.ts +2 -0
- package/src/config/resolved-config.ts +110 -12
- package/src/config/toml-loader.ts +8 -0
- package/src/config/yaml-loader.ts +9 -0
- package/src/llm/provider.ts +33 -7
- package/src/multimodal/image-loader.ts +6 -3
- package/src/rerank/http-provider.ts +125 -0
- package/src/rerank/index.ts +81 -0
- package/src/rerank/policy.ts +88 -0
- 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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
5375
|
-
const toRerank = candidates.slice(0,
|
|
5376
|
-
const rest = candidates.slice(
|
|
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
|
|
6270
|
-
|
|
6271
|
-
|
|
6272
|
-
|
|
6273
|
-
})
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6285
|
-
|
|
6286
|
-
|
|
6287
|
-
|
|
6288
|
-
|
|
6289
|
-
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
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
|
-
}
|
|
6308
|
-
|
|
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;
|
package/docs/ARCHITECTURE.md
CHANGED
|
@@ -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
|
|
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
|
|
package/docs/CONFIGURATION.md
CHANGED
|
@@ -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
|
package/docs/PERFORMANCE.md
CHANGED
|
@@ -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` |
|
|
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 |
|