@lotargo/memory_plugin 1.6.2 → 1.6.4

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.
@@ -8,8 +8,10 @@ import { buildTripleHierarchy } from "./chunker.js";
8
8
  import { embedBatch, vectorToBuffer } from "../ml/model_manager.js";
9
9
  import { buildGraphEdges, saveGraphEdges } from "../graph/graph_extractor.js";
10
10
  import { getConfig } from "../config/config_manager.js";
11
- import { assertIngestPathAllowed } from "../security/path_guard.js";
12
- import { logger } from "../logger.js";
11
+ import { assertIngestPathAllowed } from "../security/path_guard.js";
12
+ import { logger } from "../logger.js";
13
+ import { GLOBAL_KEY } from "../memory.js";
14
+ import { addDocumentScope } from "../rag_scope.js";
13
15
 
14
16
  export async function ingestDocument({
15
17
  content,
@@ -17,9 +19,10 @@ export async function ingestDocument({
17
19
  path = null,
18
20
  title = null,
19
21
  customDb = null,
20
- customBlobDir = BLOBS_DIR,
21
- generateEmbeddings = true,
22
- }) {
22
+ customBlobDir = BLOBS_DIR,
23
+ generateEmbeddings = true,
24
+ projectScope = GLOBAL_KEY,
25
+ }) {
23
26
  const db = customDb || await getDatabase();
24
27
 
25
28
  let effectiveType = type;
@@ -47,14 +50,58 @@ export async function ingestDocument({
47
50
  const { markdown, title: docTitle, metadata } = await normalizeContent({ content, type: effectiveType, path: effectivePath, title: effectiveTitle });
48
51
  if (type === "url") metadata.source_type = "url";
49
52
 
50
- const blobRes = await saveBlob(markdown, customBlobDir);
51
- const blobHash = blobRes.hash;
52
-
53
- const docId = `doc_${randomUUID().replace(/-/g, "").substring(0, 12)}`;
54
- const docPath = effectivePath || `virtual://${type}/${docId}`;
55
- const now = Date.now();
56
-
57
- const hierarchy = buildTripleHierarchy(markdown, docId, docTitle);
53
+ const blobRes = await saveBlob(markdown, customBlobDir);
54
+ const blobHash = blobRes.hash;
55
+
56
+ const generatedDocId = `doc_${randomUUID().replace(/-/g, "").substring(0, 12)}`;
57
+ const docPath = effectivePath || `virtual://${type}/${generatedDocId}`;
58
+ const existingDoc = await db.prepare("SELECT * FROM documents WHERE path = ?").get(docPath);
59
+ const docId = existingDoc?.id || generatedDocId;
60
+ const now = Date.now();
61
+
62
+ // Identical re-ingestion only adds the new project/global scope. Keeping the
63
+ // existing document id preserves every Notebook link and avoids recomputing vectors.
64
+ if (existingDoc && existingDoc.checksum === blobHash) {
65
+ await db.exec("BEGIN IMMEDIATE;");
66
+ try {
67
+ await db
68
+ .prepare("UPDATE documents SET title = ?, metadata_json = ?, updated_at = ? WHERE id = ?;")
69
+ .run(docTitle, JSON.stringify(metadata), now, docId);
70
+ const assignedScope = await addDocumentScope(db, docId, projectScope);
71
+ await db.exec("COMMIT;");
72
+
73
+ const sectionsRow = await db.prepare("SELECT COUNT(*) AS cnt FROM sections WHERE doc_id = ?").get(docId);
74
+ const chunksRow = await db.prepare("SELECT COUNT(*) AS cnt FROM micro_chunks WHERE doc_id = ?").get(docId);
75
+ if (getConfig().mode === "hybrid-sync") {
76
+ try {
77
+ const { exportDocumentData } = await import("./exporter.js");
78
+ const { enqueueSyncTask } = await import("../db/sync_queue.js");
79
+ await enqueueSyncTask("ingest_document", docId, await exportDocumentData(docId, db));
80
+ } catch (err) {
81
+ logger.error("Failed to queue document scope sync task:", err.message);
82
+ }
83
+ }
84
+ return {
85
+ docId,
86
+ doc_id: docId,
87
+ path: docPath,
88
+ blobHash,
89
+ blob_hash: blobHash,
90
+ title: docTitle,
91
+ sectionsCount: sectionsRow?.cnt || 0,
92
+ sections_count: sectionsRow?.cnt || 0,
93
+ microChunksCount: chunksRow?.cnt || 0,
94
+ micro_chunks_count: chunksRow?.cnt || 0,
95
+ deduplicated: true,
96
+ projectScope: assignedScope,
97
+ };
98
+ } catch (err) {
99
+ await db.exec("ROLLBACK;");
100
+ throw new Error(`Ingestion scope transaction failed: ${err.message}`);
101
+ }
102
+ }
103
+
104
+ const hierarchy = buildTripleHierarchy(markdown, docId, docTitle);
58
105
 
59
106
  if (generateEmbeddings && hierarchy.microChunks.length > 0) {
60
107
  const BATCH_SIZE = getConfig().batchSize || 12;
@@ -84,31 +131,49 @@ export async function ingestDocument({
84
131
  }
85
132
  }
86
133
 
87
- await db.exec("BEGIN IMMEDIATE;");
88
- try {
89
- const existingDoc = await db.prepare("SELECT id FROM documents WHERE path = ?").get(docPath);
90
- if (existingDoc) {
91
- try {
92
- await db.prepare("DELETE FROM micro_chunks_fts WHERE id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);").run(existingDoc.id);
93
- } catch {}
94
- await db.prepare("DELETE FROM graph_edges WHERE source_id = ? OR target_id = ?").run(existingDoc.id, existingDoc.id);
95
- await db.prepare("DELETE FROM documents WHERE id = ?").run(existingDoc.id);
96
- }
97
-
98
- await db.prepare(`
99
- INSERT INTO documents (id, path, blob_hash, title, checksum, toc_json, metadata_json, created_at, updated_at)
100
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
101
- `).run(
102
- docId,
103
- docPath,
104
- blobHash,
105
- docTitle,
106
- blobHash,
107
- hierarchy.toc,
108
- JSON.stringify(metadata),
109
- now,
110
- now
111
- );
134
+ await db.exec("BEGIN IMMEDIATE;");
135
+ try {
136
+ if (existingDoc) {
137
+ const ownedRows = await db.prepare(`
138
+ SELECT id FROM sections WHERE doc_id = ?
139
+ UNION SELECT id FROM medium_chunks WHERE doc_id = ?
140
+ UNION SELECT id FROM micro_chunks WHERE doc_id = ?;
141
+ `).all(docId, docId, docId);
142
+ const ownedIds = [docId, ...ownedRows.map((row) => row.id)];
143
+ try {
144
+ await db.prepare("DELETE FROM micro_chunks_fts WHERE id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);").run(docId);
145
+ } catch {}
146
+ if (ownedIds.length > 0) {
147
+ const placeholders = ownedIds.map(() => "?").join(",");
148
+ await db
149
+ .prepare(`DELETE FROM graph_edges WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders});`)
150
+ .run(...ownedIds, ...ownedIds);
151
+ }
152
+ await db.prepare("DELETE FROM micro_chunks WHERE doc_id = ?;").run(docId);
153
+ await db.prepare("DELETE FROM medium_chunks WHERE doc_id = ?;").run(docId);
154
+ await db.prepare("DELETE FROM sections WHERE doc_id = ?;").run(docId);
155
+ await db.prepare(`
156
+ UPDATE documents
157
+ SET blob_hash = ?, title = ?, checksum = ?, toc_json = ?, metadata_json = ?, updated_at = ?
158
+ WHERE id = ?;
159
+ `).run(blobHash, docTitle, blobHash, hierarchy.toc, JSON.stringify(metadata), now, docId);
160
+ } else {
161
+ await db.prepare(`
162
+ INSERT INTO documents (id, path, blob_hash, title, checksum, toc_json, metadata_json, created_at, updated_at)
163
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
164
+ `).run(
165
+ docId,
166
+ docPath,
167
+ blobHash,
168
+ docTitle,
169
+ blobHash,
170
+ hierarchy.toc,
171
+ JSON.stringify(metadata),
172
+ now,
173
+ now
174
+ );
175
+ }
176
+ const assignedScope = await addDocumentScope(db, docId, projectScope);
112
177
 
113
178
  const insertSectionStmt = db.prepare(`
114
179
  INSERT INTO sections (id, doc_id, heading, breadcrumbs, content, token_count)
@@ -142,14 +207,46 @@ export async function ingestDocument({
142
207
  await insertFtsStmt.run(micro.id, micro.content, micro.breadcrumbs);
143
208
  }
144
209
 
145
- const edges = buildGraphEdges(docId, hierarchy);
146
- await saveGraphEdges(db, edges);
147
-
148
- await db.exec("COMMIT;");
210
+ const edges = buildGraphEdges(docId, hierarchy);
211
+ await saveGraphEdges(db, edges);
212
+
213
+ // Recreate graph projections for preserved Notebook links after replacing
214
+ // the document's structural chunks. The knowledge_links rows themselves
215
+ // remain stable because the document id remains stable.
216
+ if (existingDoc) {
217
+ const links = await db.prepare("SELECT * FROM knowledge_links WHERE doc_id = ?").all(docId);
218
+ const insertLinkEdge = db.prepare(`
219
+ INSERT OR IGNORE INTO graph_edges (source_id, target_id, relation_type, metadata_json, created_at)
220
+ VALUES (?, ?, ?, ?, ?);
221
+ `);
222
+ for (const link of links) {
223
+ const targetSpec = link.start_line
224
+ ? `${docId}:L${link.start_line}-${link.end_line || link.start_line}`
225
+ : docId;
226
+ await insertLinkEdge.run(
227
+ `fact:${link.fact_key}:${link.fact_text.substring(0, 30)}`,
228
+ targetSpec,
229
+ link.relation_type || "LINKS_TO",
230
+ JSON.stringify({ linkId: link.id }),
231
+ link.created_at || now
232
+ );
233
+ }
234
+ }
235
+
236
+ await db.exec("COMMIT;");
149
237
  } catch (err) {
150
238
  await db.exec("ROLLBACK;");
151
239
  throw new Error(`Ingestion transaction failed: ${err.message}`);
152
- }
240
+ }
241
+
242
+ if (existingDoc?.blob_hash && existingDoc.blob_hash !== blobHash) {
243
+ const refs = await db.prepare("SELECT COUNT(*) AS cnt FROM documents WHERE blob_hash = ?").get(existingDoc.blob_hash);
244
+ if (!refs?.cnt) {
245
+ try {
246
+ await deleteBlob(existingDoc.blob_hash, customBlobDir);
247
+ } catch {}
248
+ }
249
+ }
153
250
 
154
251
  if (getConfig().mode === "hybrid-sync") {
155
252
  try {
@@ -172,14 +269,15 @@ export async function ingestDocument({
172
269
  sectionsCount: hierarchy.sections.length,
173
270
  sections_count: hierarchy.sections.length,
174
271
  microChunksCount: hierarchy.microChunks.length,
175
- micro_chunks_count: hierarchy.microChunks.length,
176
- deduplicated: blobRes.deduplicated,
177
- };
178
- }
272
+ micro_chunks_count: hierarchy.microChunks.length,
273
+ deduplicated: blobRes.deduplicated,
274
+ projectScope: projectScope || GLOBAL_KEY,
275
+ };
276
+ }
179
277
 
180
278
  export async function deleteDocument(docIdOrPath, customDb = null, customBlobDir = BLOBS_DIR) {
181
279
  const db = customDb || await getDatabase();
182
- const doc = await db.prepare("SELECT * FROM documents WHERE id = ? OR path = ?").get(docIdOrPath, docIdOrPath);
280
+ const doc = await db.prepare("SELECT * FROM documents WHERE id = ? OR path = ? OR title = ?").get(docIdOrPath, docIdOrPath, docIdOrPath);
183
281
  if (!doc) {
184
282
  return { deleted: false, reason: "Document not found" };
185
283
  }
@@ -10,10 +10,12 @@ const END_MARKER = "<!-- END MEMORY AGENT PROMPT -->";
10
10
  export const PROMPT_BLOCK = `${START_MARKER}
11
11
  [SYSTEM INSTRUCTION: PERSONAL CONTEXT & MEMORY AGENT]
12
12
  1. MANDATORY FIRST STEP (SESSION INITIALIZATION): At the very start of every session or conversation, your VERY FIRST ACTION MUST BE to execute \`recall\` from \`memory-agent\` with \`scope: "all"\` (or without restrictive query filters) to load ALL saved global facts, user preferences, personal context, and project guidelines BEFORE performing any other tasks or reading code. Do NOT filter or restrict the initial \`recall\` call with specific query keywords or scoped parameters, so that no global facts are missed.
13
- 2. PROACTIVE SAVING DIRECTIVE: You MUST automatically and proactively call \`remember\` from \`memory-agent\` whenever the user shares durable facts, personal preferences, coding guidelines, technology choices, or project architecture decisions. Do NOT wait for explicit user commands like "remember this".
14
- 3. SIGNAL FILTER: Save only high-signal facts (name, language, roles, constraints, tech stack preferences, architecture decisions, conventions). Translate facts into clear, concise English when saving. Do NOT save transient details or one-off conversation turns.
15
- 4. QUERY OPTIMIZATION: When using \`query_knowledge_base\` from \`memory-agent\`, transform the user's natural language question into concept-dense search queries. Do NOT send raw conversational questions ("как мне сделать X?", "что такое Y?"). Instead, extract key terms and use concise factual phrases. For multi-part queries or comparisons, use \`batch_query_knowledge_base\` with multiple targeted queries — this is more efficient (single API call, single ONNX inference pass) and yields higher retrieval quality than separate calls.
16
- 5. POLICY EXPANSION: The knowledge base automatically expands table summaries and code signatures for better recall (config \`policyExpansion\`, default: ON). If you need raw micro_chunk precision without expansion, pass \`policyExpansion: false\` per-call or set via config.${END_MARKER}`;
13
+ 2. PROJECT IDENTITY INITIALIZATION: After the initial recall, call \`memory_info\` for the current workspace. If it reports a Git identity with \`Registry: unlinked\`, immediately call \`link_project_memory\` for the current directory. If linking migrated legacy facts, repeat \`recall(scope: "all")\`. Outside a Git repository, do not create project memory; use global memory only.
14
+ 3. PROACTIVE SAVING DIRECTIVE: You MUST automatically and proactively call \`remember\` from \`memory-agent\` whenever the user shares durable facts, personal preferences, coding guidelines, technology choices, or project architecture decisions. Do NOT wait for explicit user commands like "remember this".
15
+ 4. SIGNAL FILTER: Save only high-signal facts (name, language, roles, constraints, tech stack preferences, architecture decisions, conventions). Translate facts into clear, concise English when saving. Do NOT save transient details or one-off conversation turns.
16
+ 5. QUERY OPTIMIZATION: When using \`query_knowledge_base\` from \`memory-agent\`, transform the user's natural language question into concept-dense search queries. Do NOT send raw conversational questions ("как мне сделать X?", "что такое Y?"). Instead, extract key terms and use concise factual phrases. For multi-part queries or comparisons, use \`batch_query_knowledge_base\` with multiple targeted queries — this is more efficient (single API call, single ONNX inference pass) and yields higher retrieval quality than separate calls.
17
+ 6. SELECTIVE RAG CURATION: When web research or current technical documentation yields reliable project knowledge likely to be needed again, ingest the relevant source or excerpt with project scope and link it to the project-scoped Notebook fact it supports. Use global RAG scope only for sources intentionally reusable across projects. Prioritize authoritative documentation and knowledge newer than model training. Do not ingest everything encountered, transient output, or duplicate low-value content.
18
+ 7. POLICY EXPANSION: The knowledge base automatically expands table summaries and code signatures for better recall (config \`policyExpansion\`, default: ON). If you need raw micro_chunk precision without expansion, pass \`policyExpansion: false\` per-call or set via config.${END_MARKER}`;
17
19
 
18
20
  // Plugin-owned files live here so we never destroy user-owned config content.
19
21
  const AGENT_CONFIG_DIR = join(homedir(), ".config", "memory-agent");
@@ -99,21 +101,26 @@ function buildIncludeBlock(promptFile) {
99
101
  ${END_MARKER}`;
100
102
  }
101
103
 
102
- function stripPromptBlock(content) {
103
- const startIndex = content.indexOf(START_MARKER);
104
- const endIndex = content.indexOf(END_MARKER);
105
-
106
- if (startIndex !== -1 && endIndex !== -1 && endIndex > startIndex) {
107
- const before = content.substring(0, startIndex);
108
- const after = content.substring(endIndex + END_MARKER.length);
109
- return (before + after).replace(/\n{3,}/g, "\n\n").trim();
110
- }
111
- return content.trim();
112
- }
113
-
114
- export async function enableGlobalPrompt() {
115
- const promptFile = await syncPromptFile();
116
- const targets = getGlobalPromptTargets();
104
+ export function stripPromptBlock(content) {
105
+ let clean = String(content || "");
106
+ while (true) {
107
+ const startIndex = clean.indexOf(START_MARKER);
108
+ const endIndex = clean.indexOf(END_MARKER, startIndex + START_MARKER.length);
109
+ if (startIndex === -1 || endIndex === -1 || endIndex <= startIndex) break;
110
+ clean = clean.substring(0, startIndex) + clean.substring(endIndex + END_MARKER.length);
111
+ }
112
+ return clean.replace(/\r?\n(?:[ \t]*\r?\n){2,}/g, "\n\n").trim();
113
+ }
114
+
115
+ export function upsertPromptBlock(content, block = PROMPT_BLOCK) {
116
+ const clean = stripPromptBlock(content);
117
+ return clean ? `${clean}\n\n${block}\n` : `${block}\n`;
118
+ }
119
+
120
+ export async function enableGlobalPrompt(targetNames = null) {
121
+ const promptFile = await syncPromptFile();
122
+ const requested = Array.isArray(targetNames) ? new Set(targetNames) : null;
123
+ const targets = getGlobalPromptTargets().filter((target) => !requested || requested.has(target.name));
117
124
  const state = await loadState();
118
125
  const results = [];
119
126
 
@@ -126,12 +133,10 @@ export async function enableGlobalPrompt() {
126
133
 
127
134
  const existed = existsSync(target.filePath);
128
135
  const existing = existed ? await readFile(target.filePath, "utf-8") : "";
129
- const clean = stripPromptBlock(existing);
130
-
131
- const block = target.includeSupported
132
- ? buildIncludeBlock(promptFile)
133
- : PROMPT_BLOCK;
134
- const updated = clean ? `${clean}\n\n${block}\n` : `${block}\n`;
136
+ const block = target.includeSupported
137
+ ? buildIncludeBlock(promptFile)
138
+ : PROMPT_BLOCK;
139
+ const updated = upsertPromptBlock(existing, block);
135
140
 
136
141
  const key = target.filePath;
137
142
  const prev = state[key];
@@ -10,19 +10,29 @@ export function sanitizeFtsQuery(query) {
10
10
  return words.join(" OR ");
11
11
  }
12
12
 
13
- export async function bm25Search(db, query, limit = 30) {
14
- const ftsQuery = sanitizeFtsQuery(query);
15
- if (!ftsQuery) return [];
16
-
17
- try {
18
- const stmt = db.prepare(`
19
- SELECT id, content, breadcrumbs, rank
20
- FROM micro_chunks_fts
21
- WHERE micro_chunks_fts MATCH ?
22
- ORDER BY rank
23
- LIMIT ?;
24
- `);
25
- const rows = await stmt.all(ftsQuery, limit);
13
+ export async function bm25Search(db, query, limit = 30, scopeKeys = null) {
14
+ const ftsQuery = sanitizeFtsQuery(query);
15
+ if (!ftsQuery) return [];
16
+
17
+ try {
18
+ const scoped = Array.isArray(scopeKeys) && scopeKeys.length > 0;
19
+ const scopeClause = scoped
20
+ ? `AND EXISTS (
21
+ SELECT 1 FROM micro_chunks scoped_m
22
+ JOIN document_scopes scoped_ds ON scoped_ds.doc_id = scoped_m.doc_id
23
+ WHERE scoped_m.id = micro_chunks_fts.id
24
+ AND scoped_ds.scope_key IN (${scopeKeys.map(() => "?").join(",")})
25
+ )`
26
+ : "";
27
+ const stmt = db.prepare(`
28
+ SELECT id, content, breadcrumbs, rank
29
+ FROM micro_chunks_fts
30
+ WHERE micro_chunks_fts MATCH ?
31
+ ${scopeClause}
32
+ ORDER BY rank
33
+ LIMIT ?;
34
+ `);
35
+ const rows = await stmt.all(ftsQuery, ...(scoped ? scopeKeys : []), limit);
26
36
  return rows.map((r, i) => ({
27
37
  id: r.id,
28
38
  content: r.content,
@@ -50,7 +60,7 @@ export function toVectorBytes(value) {
50
60
  return null;
51
61
  }
52
62
 
53
- export async function vectorSearch(db, queryVector, limit = 30, minSim = 0.25) {
63
+ export async function vectorSearch(db, queryVector, limit = 30, minSim = 0.25, scopeKeys = null) {
54
64
  if (!queryVector || queryVector.length === 0) return [];
55
65
 
56
66
  const vectorDim = queryVector.length;
@@ -59,21 +69,32 @@ export async function vectorSearch(db, queryVector, limit = 30, minSim = 0.25) {
59
69
  const tempVec = new Float32Array(tempBuf);
60
70
 
61
71
  const scanLimit = Number(getConfig().vectorScanLimit) || 0;
62
- const scanSql = scanLimit > 0
63
- ? `
64
- SELECT m.id, m.section_id, m.doc_id, m.content, m.vector, s.breadcrumbs
65
- FROM micro_chunks m
66
- JOIN sections s ON m.section_id = s.id
67
- LIMIT ?;
68
- `
69
- : `
70
- SELECT m.id, m.section_id, m.doc_id, m.content, m.vector, s.breadcrumbs
71
- FROM micro_chunks m
72
- JOIN sections s ON m.section_id = s.id;
73
- `;
74
-
75
- const stmt = db.prepare(scanSql);
76
- const rows = scanLimit > 0 ? await stmt.all(scanLimit) : await stmt.all();
72
+ const scoped = Array.isArray(scopeKeys) && scopeKeys.length > 0;
73
+ const scopeClause = scoped
74
+ ? `WHERE EXISTS (
75
+ SELECT 1 FROM document_scopes scoped_ds
76
+ WHERE scoped_ds.doc_id = m.doc_id
77
+ AND scoped_ds.scope_key IN (${scopeKeys.map(() => "?").join(",")})
78
+ )`
79
+ : "";
80
+ const scanSql = scanLimit > 0
81
+ ? `
82
+ SELECT m.id, m.section_id, m.doc_id, m.content, m.vector, s.breadcrumbs
83
+ FROM micro_chunks m
84
+ JOIN sections s ON m.section_id = s.id
85
+ ${scopeClause}
86
+ LIMIT ?;
87
+ `
88
+ : `
89
+ SELECT m.id, m.section_id, m.doc_id, m.content, m.vector, s.breadcrumbs
90
+ FROM micro_chunks m
91
+ JOIN sections s ON m.section_id = s.id
92
+ ${scopeClause};
93
+ `;
94
+
95
+ const stmt = db.prepare(scanSql);
96
+ const scopeParams = scoped ? scopeKeys : [];
97
+ const rows = scanLimit > 0 ? await stmt.all(...scopeParams, scanLimit) : await stmt.all(...scopeParams);
77
98
  const scored = [];
78
99
  for (const r of rows) {
79
100
  // node:sqlite returns BLOBs as plain Uint8Array (NOT Buffer), the Turso
@@ -244,7 +265,8 @@ export async function batchHybridQuery(queries, options = {}) {
244
265
  rerankerEnabled = null,
245
266
  instruction = null,
246
267
  generateEmbeddings = true,
247
- policyExpansion = null,
268
+ policyExpansion = null,
269
+ scopeKeys = null,
248
270
  } = options;
249
271
 
250
272
  const db = customDb || await getDatabase();
@@ -277,7 +299,8 @@ export async function batchHybridQuery(queries, options = {}) {
277
299
  rerankerEnabled: useReranker,
278
300
  instruction,
279
301
  generateEmbeddings,
280
- policyExpansion: usePolicyExpansion,
302
+ policyExpansion: usePolicyExpansion,
303
+ scopeKeys,
281
304
  _precomputedVector: queryVectors[i] || null,
282
305
  })
283
306
  )
@@ -299,7 +322,8 @@ export async function hybridQuery({
299
322
  rerankerEnabled = null,
300
323
  instruction = null,
301
324
  generateEmbeddings = true,
302
- policyExpansion = null, // null = use config default
325
+ policyExpansion = null, // null = use config default
326
+ scopeKeys = null, // null = all documents; tool surfaces pass global/current-project keys
303
327
  _precomputedVector = null, // internal: skip embedText if batch already computed
304
328
  }) {
305
329
  const db = customDb || await getDatabase();
@@ -327,28 +351,28 @@ export async function hybridQuery({
327
351
  let fusedHits = [];
328
352
 
329
353
  if (algo === "lexical_only" || algo === "bm25_only") {
330
- const bm25Hits = await bm25Search(db, query, limit * 4);
354
+ const bm25Hits = await bm25Search(db, query, limit * 4, scopeKeys);
331
355
  fusedHits = bm25Hits.map((hit) => ({
332
356
  ...hit,
333
357
  score: 1.0 / hit.bm25_rank,
334
358
  }));
335
359
  } else if (algo === "semantic_only" || algo === "vector_only") {
336
360
  const queryVector = await getQueryVector();
337
- const vectorHits = await vectorSearch(db, queryVector, limit * 4, 0.10);
361
+ const vectorHits = await vectorSearch(db, queryVector, limit * 4, 0.10, scopeKeys);
338
362
  fusedHits = vectorHits.map((hit) => ({
339
363
  ...hit,
340
364
  score: hit.cosine_sim,
341
365
  }));
342
366
  } else if (algo === "rrf") {
343
- const bm25Hits = await bm25Search(db, query, 30);
367
+ const bm25Hits = await bm25Search(db, query, 30, scopeKeys);
344
368
  const queryVector = await getQueryVector();
345
- const vectorHits = await vectorSearch(db, queryVector, 30, 0.10);
369
+ const vectorHits = await vectorSearch(db, queryVector, 30, 0.10, scopeKeys);
346
370
  fusedHits = rrfFusion(bm25Hits, vectorHits, 60, scoreThreshold);
347
371
  } else {
348
372
  // Default: RSF
349
- const bm25Hits = await bm25Search(db, query, 30);
373
+ const bm25Hits = await bm25Search(db, query, 30, scopeKeys);
350
374
  const queryVector = await getQueryVector();
351
- const vectorHits = await vectorSearch(db, queryVector, 30, 0.10);
375
+ const vectorHits = await vectorSearch(db, queryVector, 30, 0.10, scopeKeys);
352
376
  fusedHits = rsfFusion(bm25Hits, vectorHits, alphaWeight, scoreThreshold);
353
377
  }
354
378
 
@@ -187,32 +187,52 @@ export async function runSetup() {
187
187
  }
188
188
  }
189
189
 
190
- // 4. Codex (~/.codex/config.toml)
191
- if (doCodex) {
192
- try {
193
- const codexDir = join(home, ".codex");
194
- const codexConfig = join(codexDir, "config.toml");
195
- if (existsSync(codexDir)) {
196
- let content = existsSync(codexConfig) ? await readFile(codexConfig, "utf-8") : "";
197
- if (!content.includes("memory-agent")) {
198
- const tomlSnippet = `\n[mcp_servers.memory-agent]\ncommand = "npx"\nargs = ["-y", "@lotargo/memory_plugin"]\n`;
199
- content += tomlSnippet;
200
- await writeFile(codexConfig, content);
201
- console.log(" [OK] Codex: added mcp_servers.memory-agent to ~/.codex/config.toml");
202
- configuredCount++;
203
- } else {
204
- console.log(" [INFO] Codex: already configured");
205
- }
206
- }
207
- } catch (err) {
208
- console.log(" [SKIP] Codex setup skipped:", err.message);
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
+ } else {
220
+ console.log(" [INFO] Codex: direct Node.js memory-agent launcher already configured");
221
+ }
222
+ configuredCount++;
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
- try {
214
- const { enableGlobalPrompt } = await import("./prompt_manager.js");
215
- const promptResults = await enableGlobalPrompt();
229
+ try {
230
+ const { enableGlobalPrompt } = await import("./prompt_manager.js");
231
+ const promptTargets = [];
232
+ if (doAntigravity) promptTargets.push("Antigravity");
233
+ if (doCodex) promptTargets.push("Codex");
234
+ if (doClaude) promptTargets.push("Claude Code");
235
+ const promptResults = await enableGlobalPrompt(promptTargets);
216
236
  promptResults.forEach((r) => {
217
237
  if (r.status === "enabled") {
218
238
  console.log(` [OK] ${r.name}: enabled global prompt instruction in ${r.filePath}`);
@@ -234,14 +254,16 @@ export async function runSetup() {
234
254
  const packageSkillsDir = join(packageDir, "skills");
235
255
  if (existsSync(packageSkillsDir)) {
236
256
  const opencodeDir = process.env.OPENCODE_CONFIG_DIR || join(home, ".config", "opencode");
237
- const targets = [
238
- { name: "OpenCode", dir: join(opencodeDir, "skills") },
239
- { name: "Antigravity", dir: join(home, ".gemini", "config", "skills") },
240
- { name: "Codex", dir: join(home, ".codex", "skills") },
241
- { name: "Claude Code", dir: join(home, ".claude", "skills") },
242
- ];
243
- const cwd = process.cwd();
244
- if (existsSync(join(cwd, ".agents"))) {
257
+ const targets = [];
258
+ if (doOpenCode) targets.push({ name: "OpenCode", dir: join(opencodeDir, "skills") });
259
+ if (doAntigravity) targets.push({ name: "Antigravity", dir: join(home, ".gemini", "config", "skills") });
260
+ if (doCodex) {
261
+ targets.push({ name: "Codex", dir: join(home, ".codex", "skills") });
262
+ targets.push({ name: "Codex shared agents", dir: join(home, ".agents", "skills") });
263
+ }
264
+ if (doClaude) targets.push({ name: "Claude Code", dir: join(home, ".claude", "skills") });
265
+ const cwd = process.cwd();
266
+ if (doAntigravity && existsSync(join(cwd, ".agents"))) {
245
267
  targets.push({ name: "Antigravity (local)", dir: join(cwd, ".agents", "skills") });
246
268
  }
247
269