@lotargo/memory_plugin 1.6.5 → 1.6.7
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 +34 -0
- package/README.md +576 -443
- package/mcp-server/benchmarks/fetch_real_corpus.js +351 -0
- package/mcp-server/benchmarks/gpu_profile_benchmark.js +170 -0
- package/mcp-server/benchmarks/policy_dominance_test.js +221 -0
- package/mcp-server/benchmarks/quality_evaluator.js +598 -0
- package/mcp-server/benchmarks/raw_corpus_data.js +613 -0
- package/mcp-server/benchmarks/run_benchmarks.js +366 -0
- package/mcp-server/benchmarks/stress_ingestion.js +195 -0
- package/mcp-server/benchmarks/table_code_retrieval.js +453 -0
- package/mcp-server/benchmarks/test_dual_layer.js +141 -0
- package/mcp-server/cli/direct_commands.js +39 -0
- package/mcp-server/cli.js +16 -5
- package/mcp-server/cli_boot.js +4 -1
- package/mcp-server/client_cli.js +73 -0
- package/mcp-server/client_paths.js +44 -0
- package/mcp-server/client_registration.js +38 -0
- package/mcp-server/codex_config.js +86 -8
- package/mcp-server/db/database.js +14 -21
- package/mcp-server/db/migrations.js +66 -77
- package/mcp-server/db/rag_blob_transport.js +143 -0
- package/mcp-server/db/rag_sync.js +284 -0
- package/mcp-server/db/sync_queue.js +219 -307
- package/mcp-server/dev_link.js +142 -0
- package/mcp-server/fact_format.js +44 -12
- package/mcp-server/index.js +17 -7
- package/mcp-server/ingest/exporter.js +44 -38
- package/mcp-server/ingest/pipeline.js +260 -248
- package/mcp-server/persona_migration.js +39 -0
- package/mcp-server/prompt_manager.js +162 -55
- package/mcp-server/rag_scope.js +83 -0
- package/mcp-server/retrieval/retriever.js +99 -64
- package/mcp-server/setup.js +150 -100
- package/mcp-server/storage/blob_store.js +53 -1
- package/mcp-server/tools/core/knowledge_read_core.js +163 -0
- package/mcp-server/tools/core/memory_core.js +24 -4
- package/mcp-server/tools/core/memory_routing.js +10 -0
- package/mcp-server/tools/core/note_core.js +53 -0
- package/mcp-server/tools/core/rag_query_core.js +169 -0
- package/mcp-server/tools/index.js +11 -9
- package/mcp-server/tools/memory_tools.js +4 -1
- package/mcp-server/tools/note_tools.js +35 -0
- package/mcp-server/tools/rag_tools.js +211 -364
- package/mcp-server/uninstall.js +627 -0
- package/opencode-plugin/index.js +80 -12
- package/opencode-plugin/main.js +136 -0
- package/package.json +17 -34
- package/skills/using-memory/SKILL.md +28 -19
|
@@ -8,10 +8,38 @@ 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";
|
|
13
|
-
import { GLOBAL_KEY } from "../memory.js";
|
|
14
|
-
import { addDocumentScope } from "../rag_scope.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";
|
|
15
|
+
|
|
16
|
+
export const RAG_NOTE_KINDS = Object.freeze(["decision", "research", "context", "handoff", "note"]);
|
|
17
|
+
const RAG_NOTE_KIND_SET = new Set(RAG_NOTE_KINDS);
|
|
18
|
+
|
|
19
|
+
export function normalizeNoteKind(kind = "note") {
|
|
20
|
+
const normalized = String(kind || "note").trim().toLowerCase();
|
|
21
|
+
return RAG_NOTE_KIND_SET.has(normalized) ? normalized : "note";
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function normalizeNoteTags(tags) {
|
|
25
|
+
const values = Array.isArray(tags) ? tags : String(tags || "").split(",");
|
|
26
|
+
return [...new Set(values.map((tag) => String(tag).trim().toLowerCase()).filter(Boolean))].sort();
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function createNoteVirtualPath() {
|
|
30
|
+
return `memory://note/${randomUUID()}`;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function mergeMetadata(baseMetadata, metadataOverrides) {
|
|
34
|
+
if (!metadataOverrides || typeof metadataOverrides !== "object" || Array.isArray(metadataOverrides)) return baseMetadata;
|
|
35
|
+
return { ...baseMetadata, ...metadataOverrides };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function clearOnlyCloudTombstone(db, docId, path) {
|
|
39
|
+
if (getConfig().mode !== "only-cloud") return;
|
|
40
|
+
const { clearCloudDocumentTombstone } = await import("../db/rag_sync.js");
|
|
41
|
+
await clearCloudDocumentTombstone(db, { docId, path });
|
|
42
|
+
}
|
|
15
43
|
|
|
16
44
|
export async function ingestDocument({
|
|
17
45
|
content,
|
|
@@ -19,10 +47,11 @@ export async function ingestDocument({
|
|
|
19
47
|
path = null,
|
|
20
48
|
title = null,
|
|
21
49
|
customDb = null,
|
|
22
|
-
customBlobDir = BLOBS_DIR,
|
|
23
|
-
generateEmbeddings = true,
|
|
24
|
-
projectScope = GLOBAL_KEY,
|
|
25
|
-
|
|
50
|
+
customBlobDir = BLOBS_DIR,
|
|
51
|
+
generateEmbeddings = true,
|
|
52
|
+
projectScope = GLOBAL_KEY,
|
|
53
|
+
metadataOverrides = null,
|
|
54
|
+
}) {
|
|
26
55
|
const db = customDb || await getDatabase();
|
|
27
56
|
|
|
28
57
|
let effectiveType = type;
|
|
@@ -47,285 +76,286 @@ export async function ingestDocument({
|
|
|
47
76
|
}
|
|
48
77
|
}
|
|
49
78
|
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (
|
|
65
|
-
await db.
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
79
|
+
const normalized = await normalizeContent({
|
|
80
|
+
content,
|
|
81
|
+
type: effectiveType,
|
|
82
|
+
path: effectivePath,
|
|
83
|
+
title: effectiveTitle,
|
|
84
|
+
});
|
|
85
|
+
const markdown = normalized.markdown;
|
|
86
|
+
const docTitle = normalized.title;
|
|
87
|
+
if (type === "url") normalized.metadata.source_type = "url";
|
|
88
|
+
const metadata = mergeMetadata(normalized.metadata, metadataOverrides);
|
|
89
|
+
|
|
90
|
+
const blobRes = await saveBlob(markdown, customBlobDir);
|
|
91
|
+
const blobHash = blobRes.hash;
|
|
92
|
+
|
|
93
|
+
if (getConfig().mode === "only-cloud") {
|
|
94
|
+
const { pushBlobToCloud } = await import("../db/rag_blob_transport.js");
|
|
95
|
+
await pushBlobToCloud(db, blobHash, customBlobDir);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const generatedDocId = `doc_${randomUUID().replace(/-/g, "").substring(0, 12)}`;
|
|
99
|
+
const docPath = effectivePath || `virtual://${type}/${generatedDocId}`;
|
|
100
|
+
const existingDoc = await db.prepare("SELECT * FROM documents WHERE path = ?").get(docPath);
|
|
101
|
+
const docId = existingDoc?.id || generatedDocId;
|
|
102
|
+
const now = Date.now();
|
|
103
|
+
|
|
104
|
+
if (existingDoc && existingDoc.checksum === blobHash) {
|
|
105
|
+
let assignedScope = projectScope || GLOBAL_KEY;
|
|
106
|
+
await db.exec("BEGIN IMMEDIATE;");
|
|
107
|
+
try {
|
|
108
|
+
await db.prepare("UPDATE documents SET title = ?, metadata_json = ?, updated_at = ? WHERE id = ?;")
|
|
109
|
+
.run(docTitle, JSON.stringify(metadata), now, docId);
|
|
110
|
+
assignedScope = await addDocumentScope(db, docId, projectScope);
|
|
111
|
+
await db.exec("COMMIT;");
|
|
112
|
+
} catch (err) {
|
|
113
|
+
try { await db.exec("ROLLBACK;"); } catch {}
|
|
114
|
+
throw new Error(`Ingestion scope transaction failed: ${err.message}`);
|
|
115
|
+
}
|
|
116
|
+
await clearOnlyCloudTombstone(db, docId, docPath);
|
|
117
|
+
|
|
118
|
+
const sectionsRow = await db.prepare("SELECT COUNT(*) AS cnt FROM sections WHERE doc_id = ?").get(docId);
|
|
119
|
+
const chunksRow = await db.prepare("SELECT COUNT(*) AS cnt FROM micro_chunks WHERE doc_id = ?").get(docId);
|
|
120
|
+
if (getConfig().mode === "hybrid-sync") {
|
|
121
|
+
try {
|
|
122
|
+
const { exportDocumentData } = await import("./exporter.js");
|
|
123
|
+
const { enqueueSyncTask } = await import("../db/sync_queue.js");
|
|
124
|
+
await enqueueSyncTask("ingest_document", docId, await exportDocumentData(docId, db));
|
|
125
|
+
} catch (err) {
|
|
126
|
+
logger.error("Failed to queue document scope sync task:", err.message);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
docId, doc_id: docId, path: docPath, blobHash, blob_hash: blobHash, title: docTitle,
|
|
131
|
+
sectionsCount: sectionsRow?.cnt || 0, sections_count: sectionsRow?.cnt || 0,
|
|
132
|
+
microChunksCount: chunksRow?.cnt || 0, micro_chunks_count: chunksRow?.cnt || 0,
|
|
133
|
+
deduplicated: true, projectScope: assignedScope,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const hierarchy = buildTripleHierarchy(markdown, docId, docTitle);
|
|
105
138
|
|
|
106
139
|
if (generateEmbeddings && hierarchy.microChunks.length > 0) {
|
|
107
140
|
const BATCH_SIZE = getConfig().batchSize || 12;
|
|
108
|
-
|
|
109
|
-
// Smart Batching: Sort micro-chunks by character/token length to minimize ONNX zero-padding overhead
|
|
110
141
|
const indexedItems = hierarchy.microChunks.map((micro, idx) => ({
|
|
111
142
|
index: idx,
|
|
112
143
|
text: micro.breadcrumbs
|
|
113
144
|
? `${micro.content}\n\nContext: ${docTitle} > ${micro.breadcrumbs}`
|
|
114
145
|
: `${micro.content}\n\nContext: ${docTitle}`,
|
|
115
146
|
}));
|
|
116
|
-
|
|
117
147
|
indexedItems.sort((a, b) => a.text.length - b.text.length);
|
|
118
|
-
|
|
119
148
|
for (let i = 0; i < indexedItems.length; i += BATCH_SIZE) {
|
|
120
149
|
const batch = indexedItems.slice(i, i + BATCH_SIZE);
|
|
121
|
-
const
|
|
122
|
-
const batchVecs = await embedBatch(batchTexts, false);
|
|
150
|
+
const batchVecs = await embedBatch(batch.map((item) => item.text), false);
|
|
123
151
|
for (let j = 0; j < batchVecs.length; j++) {
|
|
124
|
-
|
|
125
|
-
hierarchy.microChunks[origIdx].vector = vectorToBuffer(batchVecs[j]);
|
|
152
|
+
hierarchy.microChunks[batch[j].index].vector = vectorToBuffer(batchVecs[j]);
|
|
126
153
|
}
|
|
127
154
|
}
|
|
128
155
|
} else {
|
|
129
|
-
for (const micro of hierarchy.microChunks)
|
|
130
|
-
micro.vector = Buffer.alloc(0);
|
|
131
|
-
}
|
|
156
|
+
for (const micro of hierarchy.microChunks) micro.vector = Buffer.alloc(0);
|
|
132
157
|
}
|
|
133
158
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
UNION SELECT id FROM
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
await db.prepare("DELETE FROM
|
|
153
|
-
await db.prepare(
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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);
|
|
177
|
-
|
|
178
|
-
const insertSectionStmt = db.prepare(`
|
|
179
|
-
INSERT INTO sections (id, doc_id, heading, breadcrumbs, content, token_count)
|
|
180
|
-
VALUES (?, ?, ?, ?, ?, ?);
|
|
181
|
-
`);
|
|
159
|
+
let assignedScope = projectScope || GLOBAL_KEY;
|
|
160
|
+
await db.exec("BEGIN IMMEDIATE;");
|
|
161
|
+
try {
|
|
162
|
+
if (existingDoc) {
|
|
163
|
+
const ownedRows = await db.prepare(`
|
|
164
|
+
SELECT id FROM sections WHERE doc_id = ?
|
|
165
|
+
UNION SELECT id FROM medium_chunks WHERE doc_id = ?
|
|
166
|
+
UNION SELECT id FROM micro_chunks WHERE doc_id = ?;
|
|
167
|
+
`).all(docId, docId, docId);
|
|
168
|
+
const ownedIds = [docId, ...ownedRows.map((row) => row.id)];
|
|
169
|
+
try { await db.prepare("DELETE FROM micro_chunks_fts WHERE id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);").run(docId); } catch {}
|
|
170
|
+
if (ownedIds.length > 0) {
|
|
171
|
+
const placeholders = ownedIds.map(() => "?").join(",");
|
|
172
|
+
await db.prepare(`DELETE FROM graph_edges WHERE source_id IN (${placeholders}) OR target_id IN (${placeholders});`)
|
|
173
|
+
.run(...ownedIds, ...ownedIds);
|
|
174
|
+
}
|
|
175
|
+
await db.prepare("DELETE FROM micro_chunks WHERE doc_id = ?;").run(docId);
|
|
176
|
+
await db.prepare("DELETE FROM medium_chunks WHERE doc_id = ?;").run(docId);
|
|
177
|
+
await db.prepare("DELETE FROM sections WHERE doc_id = ?;").run(docId);
|
|
178
|
+
await db.prepare(`UPDATE documents
|
|
179
|
+
SET blob_hash = ?, title = ?, checksum = ?, toc_json = ?, metadata_json = ?, updated_at = ?
|
|
180
|
+
WHERE id = ?;`).run(blobHash, docTitle, blobHash, hierarchy.toc, JSON.stringify(metadata), now, docId);
|
|
181
|
+
} else {
|
|
182
|
+
await db.prepare(`INSERT INTO documents
|
|
183
|
+
(id, path, blob_hash, title, checksum, toc_json, metadata_json, created_at, updated_at)
|
|
184
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`)
|
|
185
|
+
.run(docId, docPath, blobHash, docTitle, blobHash, hierarchy.toc, JSON.stringify(metadata), now, now);
|
|
186
|
+
}
|
|
187
|
+
assignedScope = await addDocumentScope(db, docId, projectScope);
|
|
188
|
+
|
|
189
|
+
const insertSectionStmt = db.prepare(`INSERT INTO sections
|
|
190
|
+
(id, doc_id, heading, breadcrumbs, content, token_count) VALUES (?, ?, ?, ?, ?, ?);`);
|
|
182
191
|
for (const sec of hierarchy.sections) {
|
|
183
192
|
await insertSectionStmt.run(sec.id, sec.doc_id, sec.heading, sec.breadcrumbs, sec.content, sec.token_count);
|
|
184
193
|
}
|
|
185
194
|
|
|
186
|
-
if (hierarchy.mediumChunks
|
|
187
|
-
const insertMediumStmt = db.prepare(`
|
|
188
|
-
|
|
189
|
-
VALUES (?, ?, ?, ?, ?, ?, ?);
|
|
190
|
-
`);
|
|
195
|
+
if (hierarchy.mediumChunks?.length) {
|
|
196
|
+
const insertMediumStmt = db.prepare(`INSERT INTO medium_chunks
|
|
197
|
+
(id, section_id, doc_id, content, block_type, token_count, created_at) VALUES (?, ?, ?, ?, ?, ?, ?);`);
|
|
191
198
|
for (const med of hierarchy.mediumChunks) {
|
|
192
199
|
await insertMediumStmt.run(med.id, med.section_id, med.doc_id, med.content, med.block_type, med.token_count, now);
|
|
193
200
|
}
|
|
194
201
|
}
|
|
195
202
|
|
|
196
|
-
const insertMicroStmt = db.prepare(`
|
|
197
|
-
|
|
198
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);
|
|
199
|
-
`);
|
|
200
|
-
const insertFtsStmt = db.prepare(`
|
|
201
|
-
INSERT INTO micro_chunks_fts (id, content, breadcrumbs)
|
|
202
|
-
VALUES (?, ?, ?);
|
|
203
|
-
`);
|
|
204
|
-
|
|
203
|
+
const insertMicroStmt = db.prepare(`INSERT INTO micro_chunks
|
|
204
|
+
(id, section_id, doc_id, content, vector, token_count, medium_id, retrieval_policy, policy_source_id)
|
|
205
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);`);
|
|
206
|
+
const insertFtsStmt = db.prepare(`INSERT INTO micro_chunks_fts (id, content, breadcrumbs) VALUES (?, ?, ?);`);
|
|
205
207
|
for (const micro of hierarchy.microChunks) {
|
|
206
208
|
await insertMicroStmt.run(micro.id, micro.section_id, micro.doc_id, micro.content, micro.vector, micro.token_count, micro.medium_id || null, micro.retrieval_policy || "micro_chunk", micro.policy_source_id || null);
|
|
207
209
|
await insertFtsStmt.run(micro.id, micro.content, micro.breadcrumbs);
|
|
208
210
|
}
|
|
209
211
|
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
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;");
|
|
212
|
+
await saveGraphEdges(db, buildGraphEdges(docId, hierarchy));
|
|
213
|
+
|
|
214
|
+
if (existingDoc) {
|
|
215
|
+
const links = await db.prepare("SELECT * FROM knowledge_links WHERE doc_id = ?").all(docId);
|
|
216
|
+
const insertLinkEdge = db.prepare(`INSERT OR IGNORE INTO graph_edges
|
|
217
|
+
(source_id, target_id, relation_type, metadata_json, created_at) VALUES (?, ?, ?, ?, ?);`);
|
|
218
|
+
for (const link of links) {
|
|
219
|
+
const targetSpec = link.start_line ? `${docId}:L${link.start_line}-${link.end_line || link.start_line}` : docId;
|
|
220
|
+
await insertLinkEdge.run(`fact:${link.fact_key}:${link.fact_text.substring(0, 30)}`, targetSpec, link.relation_type || "LINKS_TO", JSON.stringify({ linkId: link.id }), link.created_at || now);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
await db.exec("COMMIT;");
|
|
237
225
|
} catch (err) {
|
|
238
|
-
await db.exec("ROLLBACK;");
|
|
226
|
+
try { await db.exec("ROLLBACK;"); } catch {}
|
|
239
227
|
throw new Error(`Ingestion transaction failed: ${err.message}`);
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
228
|
+
}
|
|
229
|
+
await clearOnlyCloudTombstone(db, docId, docPath);
|
|
230
|
+
|
|
231
|
+
if (existingDoc?.blob_hash && existingDoc.blob_hash !== blobHash) {
|
|
232
|
+
const refs = await db.prepare("SELECT COUNT(*) AS cnt FROM documents WHERE blob_hash = ?").get(existingDoc.blob_hash);
|
|
233
|
+
if (!refs?.cnt) {
|
|
234
|
+
try { await deleteBlob(existingDoc.blob_hash, customBlobDir); } catch {}
|
|
235
|
+
if (getConfig().mode === "only-cloud") {
|
|
236
|
+
try {
|
|
237
|
+
const { deleteCloudBlobIfUnreferenced } = await import("../db/rag_blob_transport.js");
|
|
238
|
+
await deleteCloudBlobIfUnreferenced(db, existingDoc.blob_hash);
|
|
239
|
+
} catch (err) {
|
|
240
|
+
logger.error("Failed to clean replaced cloud RAG blob:", err.message);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
}
|
|
250
245
|
|
|
251
246
|
if (getConfig().mode === "hybrid-sync") {
|
|
252
247
|
try {
|
|
253
248
|
const { exportDocumentData } = await import("./exporter.js");
|
|
254
249
|
const { enqueueSyncTask } = await import("../db/sync_queue.js");
|
|
255
|
-
|
|
256
|
-
await enqueueSyncTask("ingest_document", docId, exportedData);
|
|
250
|
+
await enqueueSyncTask("ingest_document", docId, await exportDocumentData(docId, db));
|
|
257
251
|
} catch (err) {
|
|
258
252
|
logger.error("Failed to queue document ingest sync task:", err.message);
|
|
259
253
|
}
|
|
260
254
|
}
|
|
261
255
|
|
|
262
256
|
return {
|
|
263
|
-
docId,
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
257
|
+
docId, doc_id: docId, path: docPath, blobHash, blob_hash: blobHash, title: docTitle,
|
|
258
|
+
sectionsCount: hierarchy.sections.length, sections_count: hierarchy.sections.length,
|
|
259
|
+
microChunksCount: hierarchy.microChunks.length, micro_chunks_count: hierarchy.microChunks.length,
|
|
260
|
+
deduplicated: blobRes.deduplicated, projectScope: assignedScope,
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
export async function ingestNote({
|
|
265
|
+
title,
|
|
266
|
+
content,
|
|
267
|
+
kind = "note",
|
|
268
|
+
tags = null,
|
|
269
|
+
customDb = null,
|
|
270
|
+
customBlobDir = BLOBS_DIR,
|
|
271
|
+
generateEmbeddings = true,
|
|
272
|
+
projectScope = GLOBAL_KEY,
|
|
273
|
+
}) {
|
|
274
|
+
const noteTitle = String(title ?? "").trim();
|
|
275
|
+
const noteContent = String(content ?? "");
|
|
276
|
+
if (!noteTitle) throw new Error("RAG Memory Note title must not be empty");
|
|
277
|
+
if (!noteContent.trim()) throw new Error("RAG Memory Note content must not be empty");
|
|
278
|
+
|
|
279
|
+
const noteKind = normalizeNoteKind(kind);
|
|
280
|
+
const normalizedTags = normalizeNoteTags(tags);
|
|
281
|
+
const notePath = createNoteVirtualPath();
|
|
282
|
+
const noteMetadata = { source_type: "note", note_kind: noteKind, tags: normalizedTags };
|
|
283
|
+
|
|
284
|
+
const result = await ingestDocument({
|
|
285
|
+
content: noteContent,
|
|
286
|
+
type: "text",
|
|
287
|
+
path: notePath,
|
|
288
|
+
title: noteTitle,
|
|
289
|
+
customDb,
|
|
290
|
+
customBlobDir,
|
|
291
|
+
generateEmbeddings,
|
|
292
|
+
projectScope,
|
|
293
|
+
metadataOverrides: noteMetadata,
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
return {
|
|
297
|
+
...result,
|
|
298
|
+
sourceType: "note", source_type: "note", kind: noteKind, noteKind, note_kind: noteKind,
|
|
299
|
+
tags: normalizedTags, metadata: noteMetadata,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
277
302
|
|
|
278
303
|
export async function deleteDocument(docIdOrPath, customDb = null, customBlobDir = BLOBS_DIR) {
|
|
279
304
|
const db = customDb || await getDatabase();
|
|
280
|
-
const doc = await db.prepare("SELECT * FROM documents WHERE id = ? OR path = ? OR title = ?")
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
}
|
|
305
|
+
const doc = await db.prepare("SELECT * FROM documents WHERE id = ? OR path = ? OR title = ?")
|
|
306
|
+
.get(docIdOrPath, docIdOrPath, docIdOrPath);
|
|
307
|
+
if (!doc) return { deleted: false, reason: "Document not found" };
|
|
284
308
|
|
|
285
|
-
|
|
286
|
-
// (graph_edges has no FK constraints, so section/chunk/doc references would otherwise leak).
|
|
309
|
+
const deletedAt = Date.now();
|
|
287
310
|
const ownedIds = [doc.id];
|
|
288
311
|
for (const table of ["sections", "medium_chunks", "micro_chunks"]) {
|
|
289
312
|
const rows = await db.prepare(`SELECT id FROM ${table} WHERE doc_id = ?`).all(doc.id);
|
|
290
|
-
for (const
|
|
313
|
+
for (const row of rows) ownedIds.push(row.id);
|
|
291
314
|
}
|
|
292
315
|
|
|
293
316
|
await db.exec("BEGIN IMMEDIATE;");
|
|
294
317
|
try {
|
|
295
|
-
try {
|
|
296
|
-
await db.prepare("DELETE FROM micro_chunks_fts WHERE id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);").run(doc.id);
|
|
297
|
-
} catch {}
|
|
298
|
-
|
|
299
|
-
// Auto-clean Agent knowledge graph links pointing at this document.
|
|
318
|
+
try { await db.prepare("DELETE FROM micro_chunks_fts WHERE id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);").run(doc.id); } catch {}
|
|
300
319
|
await db.prepare("DELETE FROM knowledge_links WHERE doc_id = ?").run(doc.id);
|
|
301
|
-
|
|
302
320
|
for (const id of ownedIds) {
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
"DELETE FROM graph_edges WHERE source_id = ? OR target_id = ? OR source_id GLOB ? OR target_id GLOB ?"
|
|
306
|
-
).run(id, id, `${id}*`, `${id}*`);
|
|
321
|
+
await db.prepare("DELETE FROM graph_edges WHERE source_id = ? OR target_id = ? OR source_id GLOB ? OR target_id GLOB ?")
|
|
322
|
+
.run(id, id, `${id}*`, `${id}*`);
|
|
307
323
|
}
|
|
308
|
-
|
|
309
|
-
await db.prepare("DELETE FROM documents WHERE id = ?").run(doc.id);
|
|
310
|
-
|
|
324
|
+
await db.prepare("DELETE FROM documents WHERE id = ?;").run(doc.id);
|
|
311
325
|
await db.exec("COMMIT;");
|
|
312
326
|
} catch (err) {
|
|
313
|
-
await db.exec("ROLLBACK;");
|
|
327
|
+
try { await db.exec("ROLLBACK;"); } catch {}
|
|
314
328
|
throw err;
|
|
315
329
|
}
|
|
316
330
|
|
|
331
|
+
if (getConfig().mode === "only-cloud") {
|
|
332
|
+
try {
|
|
333
|
+
const { recordCloudDocumentTombstone } = await import("../db/rag_sync.js");
|
|
334
|
+
await recordCloudDocumentTombstone(db, { docId: doc.id, path: doc.path, deletedAt });
|
|
335
|
+
} catch (err) {
|
|
336
|
+
logger.error("Failed to record cloud RAG deletion tombstone:", err.message);
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
|
|
317
340
|
if (doc.blob_hash) {
|
|
318
341
|
const refCountRow = await db.prepare("SELECT COUNT(*) as cnt FROM documents WHERE blob_hash = ?").get(doc.blob_hash);
|
|
319
|
-
|
|
320
|
-
if (refCount === 0) {
|
|
342
|
+
if (Number(refCountRow?.cnt || 0) === 0) {
|
|
321
343
|
await deleteBlob(doc.blob_hash, customBlobDir);
|
|
344
|
+
if (getConfig().mode === "only-cloud") {
|
|
345
|
+
try {
|
|
346
|
+
const { deleteCloudBlobIfUnreferenced } = await import("../db/rag_blob_transport.js");
|
|
347
|
+
await deleteCloudBlobIfUnreferenced(db, doc.blob_hash);
|
|
348
|
+
} catch (err) {
|
|
349
|
+
logger.error("Failed to delete orphaned cloud RAG blob:", err.message);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
322
352
|
}
|
|
323
353
|
}
|
|
324
354
|
|
|
325
355
|
if (getConfig().mode === "hybrid-sync") {
|
|
326
356
|
try {
|
|
327
357
|
const { enqueueSyncTask } = await import("../db/sync_queue.js");
|
|
328
|
-
await enqueueSyncTask("delete_document",
|
|
358
|
+
await enqueueSyncTask("delete_document", doc.id, { path: doc.path, deletedAt });
|
|
329
359
|
} catch (err) {
|
|
330
360
|
logger.error("Failed to queue document delete sync task:", err.message);
|
|
331
361
|
}
|
|
@@ -350,71 +380,53 @@ export async function reindexEmbeddings({
|
|
|
350
380
|
const total = countRow ? countRow.cnt : 0;
|
|
351
381
|
if (total === 0) return { reindexed: 0, documentsAffected: 0, model: targetModel, dimension: targetDim };
|
|
352
382
|
|
|
353
|
-
const rows = await db.prepare(`
|
|
354
|
-
SELECT m.id, m.doc_id, m.content, s.breadcrumbs, d.title as doc_title
|
|
383
|
+
const rows = await db.prepare(`SELECT m.id, m.doc_id, m.content, s.breadcrumbs, d.title as doc_title
|
|
355
384
|
FROM micro_chunks m
|
|
356
385
|
LEFT JOIN sections s ON m.section_id = s.id
|
|
357
386
|
LEFT JOIN documents d ON m.doc_id = d.id
|
|
358
|
-
ORDER BY m.doc_id, m.id
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
? `${r.content}\n\nContext: ${r.doc_title || ""} > ${r.breadcrumbs}`
|
|
366
|
-
: `${r.content}\n\nContext: ${r.doc_title || ""}`,
|
|
387
|
+
ORDER BY m.doc_id, m.id`).all();
|
|
388
|
+
const items = rows.map((row) => ({
|
|
389
|
+
id: row.id,
|
|
390
|
+
doc_id: row.doc_id,
|
|
391
|
+
text: row.breadcrumbs
|
|
392
|
+
? `${row.content}\n\nContext: ${row.doc_title || ""} > ${row.breadcrumbs}`
|
|
393
|
+
: `${row.content}\n\nContext: ${row.doc_title || ""}`,
|
|
367
394
|
}));
|
|
368
395
|
|
|
369
|
-
const defaultEmbed = async (texts) =>
|
|
370
|
-
embedBatch(texts, false, targetModel, progressCallback, null, {}, targetDim || null);
|
|
396
|
+
const defaultEmbed = async (texts) => embedBatch(texts, false, targetModel, progressCallback, null, {}, targetDim || null);
|
|
371
397
|
const embed = embedFn || defaultEmbed;
|
|
372
|
-
|
|
373
398
|
const BATCH_SIZE = config.batchSize || 12;
|
|
374
399
|
const vectors = [];
|
|
375
400
|
for (let i = 0; i < items.length; i += BATCH_SIZE) {
|
|
376
401
|
const batch = items.slice(i, i + BATCH_SIZE);
|
|
377
|
-
const batchVecs = await embed(batch.map((
|
|
402
|
+
const batchVecs = await embed(batch.map((item) => item.text));
|
|
378
403
|
if (!batchVecs || batchVecs.length !== batch.length) {
|
|
379
404
|
throw new Error(`Embedding batch returned ${batchVecs ? batchVecs.length : 0} vectors, expected ${batch.length}`);
|
|
380
405
|
}
|
|
381
|
-
for (let j = 0; j < batch.length; j++) {
|
|
382
|
-
vectors.push({ id: batch[j].id, doc_id: batch[j].doc_id, vector: vectorToBuffer(batchVecs[j]) });
|
|
383
|
-
}
|
|
406
|
+
for (let j = 0; j < batch.length; j++) vectors.push({ id: batch[j].id, doc_id: batch[j].doc_id, vector: vectorToBuffer(batchVecs[j]) });
|
|
384
407
|
if (progressCallback) progressCallback({ done: vectors.length, total });
|
|
385
408
|
}
|
|
386
409
|
|
|
387
410
|
await db.exec("BEGIN IMMEDIATE;");
|
|
388
411
|
try {
|
|
389
412
|
const stmt = db.prepare("UPDATE micro_chunks SET vector = ? WHERE id = ?;");
|
|
390
|
-
for (const
|
|
391
|
-
await stmt.run(v.vector, v.id);
|
|
392
|
-
}
|
|
413
|
+
for (const vector of vectors) await stmt.run(vector.vector, vector.id);
|
|
393
414
|
await db.exec("COMMIT;");
|
|
394
415
|
} catch (err) {
|
|
395
|
-
await db.exec("ROLLBACK;");
|
|
416
|
+
try { await db.exec("ROLLBACK;"); } catch {}
|
|
396
417
|
throw new Error(`Re-index transaction failed: ${err.message}`);
|
|
397
418
|
}
|
|
398
419
|
|
|
399
|
-
const affectedDocIds = [...new Set(items.map((
|
|
400
|
-
|
|
420
|
+
const affectedDocIds = [...new Set(items.map((item) => item.doc_id).filter(Boolean))];
|
|
401
421
|
if (config.mode === "hybrid-sync") {
|
|
402
422
|
try {
|
|
403
423
|
const { enqueueSyncTask } = await import("../db/sync_queue.js");
|
|
404
424
|
const { exportDocumentData } = await import("./exporter.js");
|
|
405
|
-
for (const docId of affectedDocIds)
|
|
406
|
-
const exportedData = await exportDocumentData(docId, db);
|
|
407
|
-
await enqueueSyncTask("ingest_document", docId, exportedData);
|
|
408
|
-
}
|
|
425
|
+
for (const docId of affectedDocIds) await enqueueSyncTask("ingest_document", docId, await exportDocumentData(docId, db));
|
|
409
426
|
} catch (err) {
|
|
410
427
|
logger.error("Failed to queue document re-index sync tasks:", err.message);
|
|
411
428
|
}
|
|
412
429
|
}
|
|
413
430
|
|
|
414
|
-
return {
|
|
415
|
-
reindexed: vectors.length,
|
|
416
|
-
documentsAffected: affectedDocIds.length,
|
|
417
|
-
model: targetModel,
|
|
418
|
-
dimension: targetDim,
|
|
419
|
-
};
|
|
431
|
+
return { reindexed: vectors.length, documentsAffected: affectedDocIds.length, model: targetModel, dimension: targetDim };
|
|
420
432
|
}
|