@lotargo/memory_plugin 1.6.1 → 1.6.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 +42 -1
- package/README.md +59 -20
- package/mcp-server/cli/direct_commands.js +10 -1
- package/mcp-server/cli.js +2 -1
- package/mcp-server/codex_config.js +171 -0
- package/mcp-server/codex_diagnostics.js +263 -0
- package/mcp-server/config/config_manager.js +1 -0
- package/mcp-server/db/migrations.js +15 -0
- package/mcp-server/index.js +4 -2
- package/mcp-server/ingest/chunker.js +179 -12
- package/mcp-server/ingest/pipeline.js +3 -3
- package/mcp-server/prompt_manager.js +221 -218
- package/mcp-server/retrieval/retriever.js +142 -12
- package/mcp-server/setup.js +37 -21
- package/mcp-server/tools/rag_tools.js +344 -283
- package/package.json +8 -4
- package/skills/using-memory/SKILL.md +11 -1
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getDatabase } from "../db/database.js";
|
|
2
|
-
import { embedText, cosineSimilarity, rerankHits } from "../ml/model_manager.js";
|
|
2
|
+
import { embedText, embedBatch, cosineSimilarity, rerankHits } from "../ml/model_manager.js";
|
|
3
3
|
import { getConfig } from "../config/config_manager.js";
|
|
4
4
|
|
|
5
5
|
export function sanitizeFtsQuery(query) {
|
|
@@ -220,6 +220,72 @@ export function rsfFusion(bm25Hits, vectorHits, alpha = 0.5, scoreThreshold = 0.
|
|
|
220
220
|
return merged.filter((item) => item.rsf_score >= scoreThreshold);
|
|
221
221
|
}
|
|
222
222
|
|
|
223
|
+
/**
|
|
224
|
+
* Execute multiple hybrid queries in a single batch.
|
|
225
|
+
* Optimizes ONNX inference by embedding all queries in one pass.
|
|
226
|
+
* Each query still gets independent BM25 + vector search + fusion.
|
|
227
|
+
*
|
|
228
|
+
* @param {string[]} queries - Array of query strings
|
|
229
|
+
* @param {object} options - Same as hybridQuery, applied to all queries
|
|
230
|
+
* @returns {Promise<Array<Array>>} - Array of result arrays (one per query)
|
|
231
|
+
*/
|
|
232
|
+
export async function batchHybridQuery(queries, options = {}) {
|
|
233
|
+
if (!Array.isArray(queries) || queries.length === 0) return [];
|
|
234
|
+
|
|
235
|
+
const {
|
|
236
|
+
limit = 5,
|
|
237
|
+
scoreThreshold = 0.01,
|
|
238
|
+
customDb = null,
|
|
239
|
+
includeGraphContext = true,
|
|
240
|
+
fusionAlgorithm = null,
|
|
241
|
+
alpha = null,
|
|
242
|
+
embeddingModel = null,
|
|
243
|
+
rerankerModel = null,
|
|
244
|
+
rerankerEnabled = null,
|
|
245
|
+
instruction = null,
|
|
246
|
+
generateEmbeddings = true,
|
|
247
|
+
policyExpansion = null,
|
|
248
|
+
} = options;
|
|
249
|
+
|
|
250
|
+
const db = customDb || await getDatabase();
|
|
251
|
+
const activeConfig = getConfig();
|
|
252
|
+
|
|
253
|
+
const algo = fusionAlgorithm || activeConfig.fusionAlgorithm || "rsf";
|
|
254
|
+
const alphaWeight = alpha !== null && alpha !== undefined ? alpha : (activeConfig.alpha ?? 0.5);
|
|
255
|
+
const embModel = embeddingModel || activeConfig.embeddingModel || "Xenova/multilingual-e5-small";
|
|
256
|
+
const usePolicyExpansion = policyExpansion !== null && policyExpansion !== undefined ? policyExpansion : (activeConfig.policyExpansion ?? true);
|
|
257
|
+
const useReranker = rerankerEnabled !== null ? rerankerEnabled : (activeConfig.rerankerEnabled ?? false);
|
|
258
|
+
|
|
259
|
+
// Batch embed all queries in one ONNX pass (major latency saving)
|
|
260
|
+
const queryVectors = generateEmbeddings
|
|
261
|
+
? await embedBatch(queries.map((q) => q), true, embModel, null, instruction)
|
|
262
|
+
: queries.map(() => null);
|
|
263
|
+
|
|
264
|
+
// Execute all queries in parallel (BM25 + vector search are independent per query)
|
|
265
|
+
const results = await Promise.all(
|
|
266
|
+
queries.map((query, i) =>
|
|
267
|
+
hybridQuery({
|
|
268
|
+
query,
|
|
269
|
+
limit,
|
|
270
|
+
scoreThreshold,
|
|
271
|
+
customDb: db,
|
|
272
|
+
includeGraphContext,
|
|
273
|
+
fusionAlgorithm: algo,
|
|
274
|
+
alpha: alphaWeight,
|
|
275
|
+
embeddingModel: embModel,
|
|
276
|
+
rerankerModel,
|
|
277
|
+
rerankerEnabled: useReranker,
|
|
278
|
+
instruction,
|
|
279
|
+
generateEmbeddings,
|
|
280
|
+
policyExpansion: usePolicyExpansion,
|
|
281
|
+
_precomputedVector: queryVectors[i] || null,
|
|
282
|
+
})
|
|
283
|
+
)
|
|
284
|
+
);
|
|
285
|
+
|
|
286
|
+
return results;
|
|
287
|
+
}
|
|
288
|
+
|
|
223
289
|
export async function hybridQuery({
|
|
224
290
|
query,
|
|
225
291
|
limit = 5,
|
|
@@ -233,6 +299,8 @@ export async function hybridQuery({
|
|
|
233
299
|
rerankerEnabled = null,
|
|
234
300
|
instruction = null,
|
|
235
301
|
generateEmbeddings = true,
|
|
302
|
+
policyExpansion = null, // null = use config default
|
|
303
|
+
_precomputedVector = null, // internal: skip embedText if batch already computed
|
|
236
304
|
}) {
|
|
237
305
|
const db = customDb || await getDatabase();
|
|
238
306
|
const activeConfig = getConfig();
|
|
@@ -248,6 +316,13 @@ export async function hybridQuery({
|
|
|
248
316
|
const embModel = embeddingModel || activeConfig.embeddingModel || "Xenova/multilingual-e5-small";
|
|
249
317
|
const useReranker = rerankerEnabled !== null ? rerankerEnabled : (activeConfig.rerankerEnabled ?? false);
|
|
250
318
|
const rerankModelName = rerankerModel || activeConfig.rerankerModel || "Xenova/bge-reranker-base";
|
|
319
|
+
const usePolicyExpansion = policyExpansion !== null && policyExpansion !== undefined ? policyExpansion : (activeConfig.policyExpansion ?? true);
|
|
320
|
+
|
|
321
|
+
// Resolve query vector: use precomputed (from batch) or embed on demand
|
|
322
|
+
const getQueryVector = async () => {
|
|
323
|
+
if (_precomputedVector) return _precomputedVector;
|
|
324
|
+
return await embedText(query, true, embModel, null, instruction);
|
|
325
|
+
};
|
|
251
326
|
|
|
252
327
|
let fusedHits = [];
|
|
253
328
|
|
|
@@ -258,7 +333,7 @@ export async function hybridQuery({
|
|
|
258
333
|
score: 1.0 / hit.bm25_rank,
|
|
259
334
|
}));
|
|
260
335
|
} else if (algo === "semantic_only" || algo === "vector_only") {
|
|
261
|
-
const queryVector = await
|
|
336
|
+
const queryVector = await getQueryVector();
|
|
262
337
|
const vectorHits = await vectorSearch(db, queryVector, limit * 4, 0.10);
|
|
263
338
|
fusedHits = vectorHits.map((hit) => ({
|
|
264
339
|
...hit,
|
|
@@ -266,13 +341,13 @@ export async function hybridQuery({
|
|
|
266
341
|
}));
|
|
267
342
|
} else if (algo === "rrf") {
|
|
268
343
|
const bm25Hits = await bm25Search(db, query, 30);
|
|
269
|
-
const queryVector = await
|
|
344
|
+
const queryVector = await getQueryVector();
|
|
270
345
|
const vectorHits = await vectorSearch(db, queryVector, 30, 0.10);
|
|
271
346
|
fusedHits = rrfFusion(bm25Hits, vectorHits, 60, scoreThreshold);
|
|
272
347
|
} else {
|
|
273
348
|
// Default: RSF
|
|
274
349
|
const bm25Hits = await bm25Search(db, query, 30);
|
|
275
|
-
const queryVector = await
|
|
350
|
+
const queryVector = await getQueryVector();
|
|
276
351
|
const vectorHits = await vectorSearch(db, queryVector, 30, 0.10);
|
|
277
352
|
fusedHits = rsfFusion(bm25Hits, vectorHits, alphaWeight, scoreThreshold);
|
|
278
353
|
}
|
|
@@ -281,35 +356,60 @@ export async function hybridQuery({
|
|
|
281
356
|
fusedHits = await rerankHits(query, fusedHits, rerankModelName);
|
|
282
357
|
}
|
|
283
358
|
|
|
284
|
-
// Parent-Child Rollup: Deduplicate hits sharing the same medium_id or section_id to prevent noise
|
|
359
|
+
// Parent-Child Rollup: Deduplicate hits sharing the same medium_id or section_id to prevent noise.
|
|
360
|
+
// Policy chunks (table_summary, code_signature) get a distinct parent key so they coexist
|
|
361
|
+
// with micro_chunks that share the same medium_id — otherwise BM25 would lose raw rows.
|
|
285
362
|
const parentDeduplicatedHits = [];
|
|
286
363
|
if (fusedHits.length > 0) {
|
|
287
364
|
const hitIds = fusedHits.map((h) => h.id);
|
|
288
365
|
const placeholders = hitIds.map(() => "?").join(",");
|
|
289
366
|
const rows = await db.prepare(`
|
|
290
|
-
SELECT id, medium_id, section_id FROM micro_chunks WHERE id IN (${placeholders});
|
|
367
|
+
SELECT id, medium_id, section_id, retrieval_policy, policy_source_id FROM micro_chunks WHERE id IN (${placeholders});
|
|
291
368
|
`).all(...hitIds);
|
|
292
369
|
const parentMap = new Map(rows.map((r) => [r.id, r]));
|
|
293
370
|
|
|
294
371
|
const seenParents = new Set();
|
|
295
372
|
for (const hit of fusedHits) {
|
|
296
373
|
const row = parentMap.get(hit.id);
|
|
297
|
-
const
|
|
374
|
+
const isPolicy = usePolicyExpansion && (row?.retrieval_policy === "table_summary" || row?.retrieval_policy === "code_signature");
|
|
375
|
+
const baseKey = row ? (row.medium_id || row.section_id) : hit.id;
|
|
376
|
+
const parentKey = isPolicy ? `policy:${baseKey}` : `micro:${baseKey}`;
|
|
298
377
|
if (!seenParents.has(parentKey)) {
|
|
299
378
|
seenParents.add(parentKey);
|
|
300
|
-
parentDeduplicatedHits.push(hit);
|
|
379
|
+
parentDeduplicatedHits.push({ ...hit, retrieval_policy: isPolicy ? row?.retrieval_policy : "micro_chunk", policy_source_id: isPolicy ? (row?.policy_source_id || null) : null });
|
|
301
380
|
}
|
|
302
381
|
}
|
|
303
382
|
}
|
|
304
383
|
|
|
305
|
-
|
|
384
|
+
// Policy-Based Deduplication: if multiple hits resolve to the same policy_source_id, keep only the highest-scored one
|
|
385
|
+
// When policy expansion is disabled, skip — all chunks are already treated as micro_chunk
|
|
386
|
+
const policyDeduplicatedHits = usePolicyExpansion
|
|
387
|
+
? (() => {
|
|
388
|
+
const result = [];
|
|
389
|
+
const seenPolicySources = new Set();
|
|
390
|
+
for (const hit of parentDeduplicatedHits) {
|
|
391
|
+
if (hit.policy_source_id && (hit.retrieval_policy === "table_summary" || hit.retrieval_policy === "code_signature")) {
|
|
392
|
+
if (!seenPolicySources.has(hit.policy_source_id)) {
|
|
393
|
+
seenPolicySources.add(hit.policy_source_id);
|
|
394
|
+
result.push(hit);
|
|
395
|
+
}
|
|
396
|
+
} else {
|
|
397
|
+
result.push(hit);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return result;
|
|
401
|
+
})()
|
|
402
|
+
: parentDeduplicatedHits;
|
|
403
|
+
|
|
404
|
+
const topHits = policyDeduplicatedHits.slice(0, limit);
|
|
306
405
|
const results = [];
|
|
307
406
|
|
|
308
407
|
if (topHits.length > 0) {
|
|
309
408
|
const topIds = topHits.map((h) => h.id);
|
|
310
409
|
const placeholders = topIds.map(() => "?").join(",");
|
|
311
410
|
const details = await db.prepare(`
|
|
312
|
-
SELECT m.id as micro_id,
|
|
411
|
+
SELECT m.id as micro_id, m.retrieval_policy, m.policy_source_id,
|
|
412
|
+
s.id as section_id, s.heading, s.breadcrumbs, s.content as section_content,
|
|
313
413
|
med.content as medium_content, d.title as doc_title, d.path as doc_path
|
|
314
414
|
FROM micro_chunks m
|
|
315
415
|
JOIN sections s ON m.section_id = s.id
|
|
@@ -329,26 +429,56 @@ export async function hybridQuery({
|
|
|
329
429
|
}
|
|
330
430
|
}
|
|
331
431
|
|
|
432
|
+
// Fetch full content for policy-based expansion (table_summary → full table, code_signature → full code)
|
|
433
|
+
// When policy expansion is disabled, skip — snippets stay as micro_chunk content
|
|
434
|
+
const policySourceIds = usePolicyExpansion
|
|
435
|
+
? details
|
|
436
|
+
.filter((d) => d.policy_source_id && (d.retrieval_policy === "table_summary" || d.retrieval_policy === "code_signature"))
|
|
437
|
+
.map((d) => d.policy_source_id)
|
|
438
|
+
: [];
|
|
439
|
+
const uniquePolicySourceIds = [...new Set(policySourceIds)];
|
|
440
|
+
|
|
441
|
+
let expandedContentMap = new Map();
|
|
442
|
+
if (uniquePolicySourceIds.length > 0) {
|
|
443
|
+
const policyPlaceholders = uniquePolicySourceIds.map(() => "?").join(",");
|
|
444
|
+
const expandedRows = await db.prepare(`
|
|
445
|
+
SELECT id, content, block_type FROM medium_chunks WHERE id IN (${policyPlaceholders});
|
|
446
|
+
`).all(...uniquePolicySourceIds);
|
|
447
|
+
expandedContentMap = new Map(expandedRows.map((r) => [r.id, r]));
|
|
448
|
+
}
|
|
449
|
+
|
|
332
450
|
for (const hit of topHits) {
|
|
333
451
|
const detail = detailMap.get(hit.id);
|
|
334
452
|
if (!detail) continue;
|
|
335
453
|
|
|
336
454
|
const symbols = symbolsBySection.get(detail.section_id) || [];
|
|
337
455
|
|
|
456
|
+
let snippet = hit.content;
|
|
457
|
+
let paragraphContext = detail.medium_content || hit.content;
|
|
458
|
+
|
|
459
|
+
if (usePolicyExpansion && detail.policy_source_id && (detail.retrieval_policy === "table_summary" || detail.retrieval_policy === "code_signature")) {
|
|
460
|
+
const expanded = expandedContentMap.get(detail.policy_source_id);
|
|
461
|
+
if (expanded) {
|
|
462
|
+
snippet = expanded.content;
|
|
463
|
+
paragraphContext = expanded.content;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
338
467
|
results.push({
|
|
339
468
|
chunk_id: hit.id,
|
|
340
469
|
doc_title: detail.doc_title,
|
|
341
470
|
doc_path: detail.doc_path,
|
|
342
471
|
heading: detail.heading,
|
|
343
472
|
breadcrumbs: detail.breadcrumbs,
|
|
344
|
-
snippet
|
|
345
|
-
paragraph_context:
|
|
473
|
+
snippet,
|
|
474
|
+
paragraph_context: paragraphContext,
|
|
346
475
|
full_section_content: detail.section_content,
|
|
347
476
|
score: parseFloat((hit.score || 0).toFixed(4)),
|
|
348
477
|
rsf_score: hit.rsf_score ? parseFloat(hit.rsf_score.toFixed(4)) : null,
|
|
349
478
|
rrf_score: hit.rrf_score ? parseFloat(hit.rrf_score.toFixed(4)) : null,
|
|
350
479
|
cosine_sim: hit.cosine_sim ? parseFloat(hit.cosine_sim.toFixed(4)) : null,
|
|
351
480
|
defined_symbols: symbols,
|
|
481
|
+
retrieval_policy: (usePolicyExpansion ? (detail.retrieval_policy || "micro_chunk") : "micro_chunk"),
|
|
352
482
|
});
|
|
353
483
|
}
|
|
354
484
|
}
|
package/mcp-server/setup.js
CHANGED
|
@@ -187,27 +187,43 @@ export async function runSetup() {
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
// 4. Codex (~/.codex/config.toml)
|
|
191
|
-
if (doCodex) {
|
|
192
|
-
try {
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
190
|
+
// 4. Codex (~/.codex/config.toml)
|
|
191
|
+
if (doCodex) {
|
|
192
|
+
try {
|
|
193
|
+
const {
|
|
194
|
+
updateCodexMemoryAgentConfig,
|
|
195
|
+
validateCodexRuntime,
|
|
196
|
+
} = await import("./codex_config.js");
|
|
197
|
+
const codexDir = join(home, ".codex");
|
|
198
|
+
const codexConfig = join(codexDir, "config.toml");
|
|
199
|
+
const nodePath = process.execPath;
|
|
200
|
+
const bootPath = fileURLToPath(new URL("./boot.js", import.meta.url));
|
|
201
|
+
const runtime = validateCodexRuntime({ nodePath, nodeVersion: process.versions.node, bootPath });
|
|
202
|
+
if (!runtime.ok) {
|
|
203
|
+
throw new Error(`Codex direct launcher validation failed: ${runtime.errors.join("; ")}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
await mkdir(codexDir, { recursive: true });
|
|
207
|
+
const content = existsSync(codexConfig) ? await readFile(codexConfig, "utf-8") : "";
|
|
208
|
+
const update = updateCodexMemoryAgentConfig(content, { nodePath, bootPath });
|
|
209
|
+
if (update.status === "conflict") {
|
|
210
|
+
throw new Error(update.reason);
|
|
211
|
+
}
|
|
212
|
+
if (update.changed) {
|
|
213
|
+
await writeFile(codexConfig, update.content, "utf-8");
|
|
214
|
+
console.log(
|
|
215
|
+
update.status === "added"
|
|
216
|
+
? " [OK] Codex: added direct Node.js memory-agent launcher to ~/.codex/config.toml"
|
|
217
|
+
: " [OK] Codex: migrated memory-agent to a direct Node.js launcher in ~/.codex/config.toml"
|
|
218
|
+
);
|
|
219
|
+
configuredCount++;
|
|
220
|
+
} else {
|
|
221
|
+
console.log(" [INFO] Codex: direct Node.js memory-agent launcher already configured");
|
|
222
|
+
}
|
|
223
|
+
} catch (err) {
|
|
224
|
+
console.log(" [FAIL] Codex setup failed:", err.message);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
211
227
|
|
|
212
228
|
// 5. Global Prompt Instructions (Antigravity, Codex, Claude Code)
|
|
213
229
|
try {
|