@lotargo/memory_plugin 1.6.3 → 1.6.5
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 +64 -1
- package/README.md +28 -20
- package/mcp-server/admin/snapshot.js +93 -26
- package/mcp-server/cli/direct_commands.js +5 -3
- package/mcp-server/cli/handlers/storage_actions.js +3 -1
- package/mcp-server/config/config_manager.js +0 -1
- package/mcp-server/db/migrations.js +26 -5
- package/mcp-server/db/sync_queue.js +118 -44
- package/mcp-server/graph/knowledge_linker.js +126 -19
- package/mcp-server/ingest/exporter.js +38 -9
- package/mcp-server/ingest/pipeline.js +146 -48
- package/mcp-server/memory.js +13 -3
- package/mcp-server/prompt_manager.js +10 -7
- package/mcp-server/retrieval/retriever.js +62 -38
- package/mcp-server/setup.js +18 -12
- package/mcp-server/tools/core/memory_core.js +465 -393
- package/mcp-server/tools/identity_tools.js +25 -6
- package/mcp-server/tools/memory_tools.js +138 -123
- package/mcp-server/tools/rag_tools.js +313 -249
- package/opencode-plugin/index.js +558 -440
- package/package.json +5 -5
- package/skills/using-memory/SKILL.md +152 -117
|
@@ -3,7 +3,8 @@ import { join } from "path";
|
|
|
3
3
|
import { MEMORY_DIR, GLOBAL_KEY, buildMemoryContent, extractFacts, writeMemoryFile } from "../memory.js";
|
|
4
4
|
import { toVectorBytes } from "../retrieval/retriever.js";
|
|
5
5
|
|
|
6
|
-
let isSyncing = false;
|
|
6
|
+
let isSyncing = false;
|
|
7
|
+
let syncRequested = false;
|
|
7
8
|
|
|
8
9
|
// Reverse sync (cloud -> local) throttling: only pull at most once per window
|
|
9
10
|
// even if readMemory triggers it frequently (recall hits every keystroke).
|
|
@@ -30,13 +31,24 @@ async function processSyncTask(db, task) {
|
|
|
30
31
|
sql: "SELECT id FROM documents WHERE id = ? OR path = ?;",
|
|
31
32
|
args: [docId, docId],
|
|
32
33
|
});
|
|
33
|
-
if (docRow.rows.length > 0) {
|
|
34
|
-
const realDocId = docRow.rows[0].id;
|
|
35
|
-
await db.cloudClient.execute({
|
|
34
|
+
if (docRow.rows.length > 0) {
|
|
35
|
+
const realDocId = docRow.rows[0].id;
|
|
36
|
+
await db.cloudClient.execute({
|
|
37
|
+
sql: `DELETE FROM graph_edges
|
|
38
|
+
WHERE source_id = ? OR target_id = ?
|
|
39
|
+
OR target_id GLOB ?
|
|
40
|
+
OR source_id IN (SELECT id FROM sections WHERE doc_id = ?)
|
|
41
|
+
OR target_id IN (SELECT id FROM sections WHERE doc_id = ?)
|
|
42
|
+
OR source_id IN (SELECT id FROM medium_chunks WHERE doc_id = ?)
|
|
43
|
+
OR target_id IN (SELECT id FROM medium_chunks WHERE doc_id = ?)
|
|
44
|
+
OR source_id IN (SELECT id FROM micro_chunks WHERE doc_id = ?)
|
|
45
|
+
OR target_id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);`,
|
|
46
|
+
args: [realDocId, realDocId, `${realDocId}:L*`, realDocId, realDocId, realDocId, realDocId, realDocId, realDocId],
|
|
47
|
+
});
|
|
48
|
+
await db.cloudClient.execute({ sql: "DELETE FROM micro_chunks_fts WHERE id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);", args: [realDocId] });
|
|
36
49
|
await db.cloudClient.execute({ sql: "DELETE FROM micro_chunks WHERE doc_id = ?;", args: [realDocId] });
|
|
37
50
|
await db.cloudClient.execute({ sql: "DELETE FROM medium_chunks WHERE doc_id = ?;", args: [realDocId] });
|
|
38
51
|
await db.cloudClient.execute({ sql: "DELETE FROM sections WHERE doc_id = ?;", args: [realDocId] });
|
|
39
|
-
await db.cloudClient.execute({ sql: "DELETE FROM graph_edges WHERE source_id = ? OR target_id = ?;", args: [realDocId, realDocId] });
|
|
40
52
|
await db.cloudClient.execute({ sql: "DELETE FROM knowledge_links WHERE doc_id = ?;", args: [realDocId] });
|
|
41
53
|
await db.cloudClient.execute({ sql: "DELETE FROM documents WHERE id = ?;", args: [realDocId] });
|
|
42
54
|
}
|
|
@@ -52,14 +64,25 @@ async function processSyncTask(db, task) {
|
|
|
52
64
|
sql: "SELECT id FROM documents WHERE id = ? OR path = ?;",
|
|
53
65
|
args: [doc.id, doc.path],
|
|
54
66
|
});
|
|
55
|
-
if (existingDocRow.rows.length > 0) {
|
|
56
|
-
const realDocId = existingDocRow.rows[0].id;
|
|
57
|
-
await db.cloudClient.execute({
|
|
67
|
+
if (existingDocRow.rows.length > 0) {
|
|
68
|
+
const realDocId = existingDocRow.rows[0].id;
|
|
69
|
+
await db.cloudClient.execute({
|
|
70
|
+
sql: `DELETE FROM graph_edges
|
|
71
|
+
WHERE source_id = ? OR target_id = ?
|
|
72
|
+
OR target_id GLOB ?
|
|
73
|
+
OR source_id IN (SELECT id FROM sections WHERE doc_id = ?)
|
|
74
|
+
OR target_id IN (SELECT id FROM sections WHERE doc_id = ?)
|
|
75
|
+
OR source_id IN (SELECT id FROM medium_chunks WHERE doc_id = ?)
|
|
76
|
+
OR target_id IN (SELECT id FROM medium_chunks WHERE doc_id = ?)
|
|
77
|
+
OR source_id IN (SELECT id FROM micro_chunks WHERE doc_id = ?)
|
|
78
|
+
OR target_id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);`,
|
|
79
|
+
args: [realDocId, realDocId, `${realDocId}:L*`, realDocId, realDocId, realDocId, realDocId, realDocId, realDocId],
|
|
80
|
+
});
|
|
81
|
+
await db.cloudClient.execute({ sql: "DELETE FROM micro_chunks_fts WHERE id IN (SELECT id FROM micro_chunks WHERE doc_id = ?);", args: [realDocId] });
|
|
58
82
|
await db.cloudClient.execute({ sql: "DELETE FROM micro_chunks WHERE doc_id = ?;", args: [realDocId] });
|
|
59
83
|
await db.cloudClient.execute({ sql: "DELETE FROM medium_chunks WHERE doc_id = ?;", args: [realDocId] });
|
|
60
84
|
await db.cloudClient.execute({ sql: "DELETE FROM sections WHERE doc_id = ?;", args: [realDocId] });
|
|
61
|
-
await db.cloudClient.execute({ sql: "DELETE FROM
|
|
62
|
-
await db.cloudClient.execute({ sql: "DELETE FROM knowledge_links WHERE doc_id = ?;", args: [realDocId] });
|
|
85
|
+
await db.cloudClient.execute({ sql: "DELETE FROM knowledge_links WHERE doc_id = ?;", args: [realDocId] });
|
|
63
86
|
await db.cloudClient.execute({ sql: "DELETE FROM documents WHERE id = ?;", args: [realDocId] });
|
|
64
87
|
}
|
|
65
88
|
|
|
@@ -79,8 +102,18 @@ async function processSyncTask(db, task) {
|
|
|
79
102
|
doc.metadata_json ? (typeof doc.metadata_json === "string" ? doc.metadata_json : JSON.stringify(doc.metadata_json)) : null,
|
|
80
103
|
doc.created_at,
|
|
81
104
|
doc.updated_at,
|
|
82
|
-
],
|
|
83
|
-
});
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
const scopes = Array.isArray(data.document_scopes) && data.document_scopes.length
|
|
109
|
+
? data.document_scopes
|
|
110
|
+
: [{ scope_key: "global", created_at: doc.created_at }];
|
|
111
|
+
for (const scope of scopes) {
|
|
112
|
+
await db.cloudClient.execute({
|
|
113
|
+
sql: "INSERT OR IGNORE INTO document_scopes (doc_id, scope_key, created_at) VALUES (?, ?, ?);",
|
|
114
|
+
args: [doc.id, scope.scope_key || "global", scope.created_at || Date.now()],
|
|
115
|
+
});
|
|
116
|
+
}
|
|
84
117
|
|
|
85
118
|
// 3. Insert sections
|
|
86
119
|
if (Array.isArray(data.sections)) {
|
|
@@ -112,8 +145,18 @@ async function processSyncTask(db, task) {
|
|
|
112
145
|
? Buffer.from(vecBytes.buffer, vecBytes.byteOffset, vecBytes.byteLength)
|
|
113
146
|
: Buffer.alloc(0);
|
|
114
147
|
await db.cloudClient.execute({
|
|
115
|
-
sql: "INSERT INTO micro_chunks (id, section_id, doc_id, content, vector, token_count, medium_id) VALUES (?, ?, ?, ?, ?, ?, ?);",
|
|
116
|
-
args: [
|
|
148
|
+
sql: "INSERT INTO micro_chunks (id, section_id, doc_id, content, vector, token_count, medium_id, retrieval_policy, policy_source_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);",
|
|
149
|
+
args: [
|
|
150
|
+
mc.id,
|
|
151
|
+
mc.section_id,
|
|
152
|
+
doc.id,
|
|
153
|
+
mc.content,
|
|
154
|
+
vecBuf,
|
|
155
|
+
mc.token_count,
|
|
156
|
+
mc.medium_id || null,
|
|
157
|
+
mc.retrieval_policy || "micro_chunk",
|
|
158
|
+
mc.policy_source_id || null,
|
|
159
|
+
],
|
|
117
160
|
});
|
|
118
161
|
|
|
119
162
|
// Insert into remote FTS
|
|
@@ -129,16 +172,38 @@ async function processSyncTask(db, task) {
|
|
|
129
172
|
}
|
|
130
173
|
|
|
131
174
|
// 6. Insert graph_edges
|
|
132
|
-
if (Array.isArray(data.graph_edges)) {
|
|
175
|
+
if (Array.isArray(data.graph_edges)) {
|
|
133
176
|
for (const e of data.graph_edges) {
|
|
134
177
|
await db.cloudClient.execute({
|
|
135
178
|
sql: "INSERT OR IGNORE INTO graph_edges (source_id, target_id, relation_type, metadata_json, created_at) VALUES (?, ?, ?, ?, ?);",
|
|
136
179
|
args: [e.source_id, e.target_id, e.relation_type, e.metadata_json ? (typeof e.metadata_json === "string" ? e.metadata_json : JSON.stringify(e.metadata_json)) : null, e.created_at || Date.now()],
|
|
137
180
|
});
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (Array.isArray(data.knowledge_links)) {
|
|
185
|
+
for (const link of data.knowledge_links) {
|
|
186
|
+
await db.cloudClient.execute({
|
|
187
|
+
sql: `INSERT OR REPLACE INTO knowledge_links
|
|
188
|
+
(id, fact_key, fact_text, doc_id, section_id, start_line, end_line, relation_type, metadata_json, created_at)
|
|
189
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?);`,
|
|
190
|
+
args: [
|
|
191
|
+
link.id,
|
|
192
|
+
link.fact_key,
|
|
193
|
+
link.fact_text,
|
|
194
|
+
doc.id,
|
|
195
|
+
link.section_id || null,
|
|
196
|
+
link.start_line || null,
|
|
197
|
+
link.end_line || null,
|
|
198
|
+
link.relation_type || "LINKS_TO",
|
|
199
|
+
link.metadata_json || null,
|
|
200
|
+
link.created_at || Date.now(),
|
|
201
|
+
],
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
142
207
|
|
|
143
208
|
export async function enqueueSyncTask(action, keyOrId, payload = null) {
|
|
144
209
|
const { getDatabase } = await import("./database.js");
|
|
@@ -317,9 +382,13 @@ export function resetReverseSyncThrottle() {
|
|
|
317
382
|
lastReverseSync = 0;
|
|
318
383
|
}
|
|
319
384
|
|
|
320
|
-
export async function triggerBackgroundSync() {
|
|
321
|
-
if (isSyncing)
|
|
322
|
-
|
|
385
|
+
export async function triggerBackgroundSync() {
|
|
386
|
+
if (isSyncing) {
|
|
387
|
+
syncRequested = true;
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
isSyncing = true;
|
|
391
|
+
syncRequested = false;
|
|
323
392
|
|
|
324
393
|
try {
|
|
325
394
|
const { getDatabase } = await import("./database.js");
|
|
@@ -340,30 +409,35 @@ export async function triggerBackgroundSync() {
|
|
|
340
409
|
);
|
|
341
410
|
`);
|
|
342
411
|
|
|
343
|
-
//
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
412
|
+
// Drain until empty. New tasks can be enqueued while a batch is running;
|
|
413
|
+
// stopping after one snapshot of the queue left those tasks stranded until
|
|
414
|
+
// an unrelated future write happened to wake the worker again.
|
|
415
|
+
let syncFailed = false;
|
|
416
|
+
while (!syncFailed) {
|
|
417
|
+
const tasks = await db.prepare("SELECT * FROM sync_queue ORDER BY id ASC LIMIT 50;").all();
|
|
418
|
+
if (tasks.length === 0) break;
|
|
419
|
+
|
|
420
|
+
for (const task of tasks) {
|
|
421
|
+
try {
|
|
422
|
+
await processSyncTask(db, task);
|
|
423
|
+
await db.prepare("DELETE FROM sync_queue WHERE id = ?;").run(task.id);
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.error(`Failed to process sync task ${task.id} (${task.action}):`, err.message, err.stack);
|
|
426
|
+
syncFailed = true;
|
|
427
|
+
break;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
}
|
|
361
431
|
|
|
362
432
|
// Push queue drained — now pull cloud state back down (reverse sync).
|
|
363
433
|
await syncFromCloud();
|
|
364
434
|
} catch (err) {
|
|
365
435
|
console.error("Error during background sync execution:", err.message);
|
|
366
|
-
} finally {
|
|
367
|
-
isSyncing = false;
|
|
368
|
-
|
|
369
|
-
|
|
436
|
+
} finally {
|
|
437
|
+
isSyncing = false;
|
|
438
|
+
if (syncRequested) {
|
|
439
|
+
syncRequested = false;
|
|
440
|
+
await triggerBackgroundSync();
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import { getDatabase } from "../db/database.js";
|
|
2
|
-
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { GLOBAL_KEY } from "../memory.js";
|
|
4
|
+
|
|
5
|
+
export async function queueDocumentSyncIfNeeded(db, docId) {
|
|
6
|
+
try {
|
|
7
|
+
const { getConfig } = await import("../config/config_manager.js");
|
|
8
|
+
if (getConfig().mode !== "hybrid-sync") return;
|
|
9
|
+
const { exportDocumentData } = await import("../ingest/exporter.js");
|
|
10
|
+
const { enqueueSyncTask } = await import("../db/sync_queue.js");
|
|
11
|
+
await enqueueSyncTask("ingest_document", docId, await exportDocumentData(docId, db));
|
|
12
|
+
} catch (err) {
|
|
13
|
+
console.warn(`Failed to queue linked RAG document sync for ${docId}: ${err.message}`);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
3
16
|
|
|
4
17
|
export async function linkFactToDocument({
|
|
5
18
|
factKey,
|
|
@@ -15,9 +28,19 @@ export async function linkFactToDocument({
|
|
|
15
28
|
const id = `link_${randomUUID().substring(0, 12)}`;
|
|
16
29
|
const now = Date.now();
|
|
17
30
|
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
|
|
31
|
+
const allowedScopes = factKey === GLOBAL_KEY ? [GLOBAL_KEY] : [GLOBAL_KEY, factKey];
|
|
32
|
+
const placeholders = allowedScopes.map(() => "?").join(",");
|
|
33
|
+
const doc = await db
|
|
34
|
+
.prepare(`
|
|
35
|
+
SELECT d.id, d.title, d.path
|
|
36
|
+
FROM documents d
|
|
37
|
+
WHERE (d.id = ? OR d.path = ? OR d.title = ?)
|
|
38
|
+
AND EXISTS (
|
|
39
|
+
SELECT 1 FROM document_scopes ds
|
|
40
|
+
WHERE ds.doc_id = d.id AND ds.scope_key IN (${placeholders})
|
|
41
|
+
)
|
|
42
|
+
`)
|
|
43
|
+
.get(docId, docId, docId, ...allowedScopes);
|
|
21
44
|
|
|
22
45
|
if (!doc) {
|
|
23
46
|
throw new Error(`Target document not found in knowledge base for link: ${docId}`);
|
|
@@ -46,7 +69,8 @@ export async function linkFactToDocument({
|
|
|
46
69
|
VALUES (?, ?, ?, ?, ?)
|
|
47
70
|
`);
|
|
48
71
|
const targetSpec = startLine ? `${doc.id}:L${startLine}-${endLine || startLine}` : doc.id;
|
|
49
|
-
await edgeStmt.run(`fact:${factKey}:${factText.substring(0, 30)}`, targetSpec, relationType, JSON.stringify({ linkId: id }), now);
|
|
72
|
+
await edgeStmt.run(`fact:${factKey}:${factText.substring(0, 30)}`, targetSpec, relationType, JSON.stringify({ linkId: id }), now);
|
|
73
|
+
await queueDocumentSyncIfNeeded(db, doc.id);
|
|
50
74
|
|
|
51
75
|
return {
|
|
52
76
|
linkId: id,
|
|
@@ -73,19 +97,33 @@ export async function getLinksForFact(factKey, factText) {
|
|
|
73
97
|
return await stmt.all(factKey, queryPattern, factText);
|
|
74
98
|
}
|
|
75
99
|
|
|
76
|
-
export async function getLinksForDoc(docId) {
|
|
77
|
-
const db = await getDatabase();
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
100
|
+
export async function getLinksForDoc(docId, scopeKeys = null) {
|
|
101
|
+
const db = await getDatabase();
|
|
102
|
+
const scoped = Array.isArray(scopeKeys) && scopeKeys.length > 0;
|
|
103
|
+
const scopeClause = scoped
|
|
104
|
+
? `AND k.fact_key IN (${scopeKeys.map(() => "?").join(",")})
|
|
105
|
+
AND EXISTS (
|
|
106
|
+
SELECT 1 FROM document_scopes ds
|
|
107
|
+
WHERE ds.doc_id = d.id AND ds.scope_key IN (${scopeKeys.map(() => "?").join(",")})
|
|
108
|
+
)`
|
|
109
|
+
: "";
|
|
110
|
+
const stmt = db.prepare(`
|
|
111
|
+
SELECT k.*, d.title as doc_title, d.path as doc_path
|
|
112
|
+
FROM knowledge_links k
|
|
113
|
+
JOIN documents d ON k.doc_id = d.id
|
|
114
|
+
WHERE (k.doc_id = ? OR d.path = ? OR d.title = ?)
|
|
115
|
+
${scopeClause}
|
|
116
|
+
ORDER BY k.created_at DESC
|
|
117
|
+
`);
|
|
118
|
+
return await stmt.all(
|
|
119
|
+
docId,
|
|
120
|
+
docId,
|
|
121
|
+
docId,
|
|
122
|
+
...(scoped ? [...scopeKeys, ...scopeKeys] : [])
|
|
123
|
+
);
|
|
124
|
+
}
|
|
87
125
|
|
|
88
|
-
export async function listAllLinks(factKey = null) {
|
|
126
|
+
export async function listAllLinks(factKey = null) {
|
|
89
127
|
const db = await getDatabase();
|
|
90
128
|
let sql = `
|
|
91
129
|
SELECT k.*, d.title as doc_title, d.path as doc_path
|
|
@@ -98,5 +136,74 @@ export async function listAllLinks(factKey = null) {
|
|
|
98
136
|
params.push(factKey);
|
|
99
137
|
}
|
|
100
138
|
sql += " ORDER BY k.created_at DESC";
|
|
101
|
-
return await db.prepare(sql).all(...params);
|
|
102
|
-
}
|
|
139
|
+
return await db.prepare(sql).all(...params);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export async function moveKnowledgeScope(db, sourceKey, targetKey) {
|
|
143
|
+
if (!sourceKey || !targetKey || sourceKey === targetKey) {
|
|
144
|
+
return { movedLinks: 0, movedDocuments: 0 };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const links = await db.prepare("SELECT * FROM knowledge_links WHERE fact_key = ?").all(sourceKey);
|
|
148
|
+
const scopedDocs = await db.prepare("SELECT doc_id, created_at FROM document_scopes WHERE scope_key = ?").all(sourceKey);
|
|
149
|
+
const docIds = new Set([...scopedDocs.map((row) => row.doc_id), ...links.map((link) => link.doc_id)]);
|
|
150
|
+
|
|
151
|
+
for (const docId of docIds) {
|
|
152
|
+
await db
|
|
153
|
+
.prepare("INSERT OR IGNORE INTO document_scopes (doc_id, scope_key, created_at) VALUES (?, ?, ?)")
|
|
154
|
+
.run(docId, targetKey, Date.now());
|
|
155
|
+
}
|
|
156
|
+
await db.prepare("DELETE FROM document_scopes WHERE scope_key = ?").run(sourceKey);
|
|
157
|
+
|
|
158
|
+
for (const link of links) {
|
|
159
|
+
const oldSource = `fact:${sourceKey}:${link.fact_text.substring(0, 30)}`;
|
|
160
|
+
const targetSpec = link.start_line
|
|
161
|
+
? `${link.doc_id}:L${link.start_line}-${link.end_line || link.start_line}`
|
|
162
|
+
: link.doc_id;
|
|
163
|
+
await db
|
|
164
|
+
.prepare("DELETE FROM graph_edges WHERE source_id = ? AND target_id = ? AND relation_type = ?")
|
|
165
|
+
.run(oldSource, targetSpec, link.relation_type || "LINKS_TO");
|
|
166
|
+
const newSource = `fact:${targetKey}:${link.fact_text.substring(0, 30)}`;
|
|
167
|
+
await db.prepare(`
|
|
168
|
+
INSERT OR IGNORE INTO graph_edges (source_id, target_id, relation_type, metadata_json, created_at)
|
|
169
|
+
VALUES (?, ?, ?, ?, ?)
|
|
170
|
+
`).run(
|
|
171
|
+
newSource,
|
|
172
|
+
targetSpec,
|
|
173
|
+
link.relation_type || "LINKS_TO",
|
|
174
|
+
JSON.stringify({ linkId: link.id }),
|
|
175
|
+
link.created_at || Date.now()
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
await db.prepare("UPDATE knowledge_links SET fact_key = ? WHERE fact_key = ?").run(targetKey, sourceKey);
|
|
179
|
+
|
|
180
|
+
for (const docId of docIds) {
|
|
181
|
+
await queueDocumentSyncIfNeeded(db, docId);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return { movedLinks: links.length, movedDocuments: docIds.size };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export async function deleteLinksForFacts(db, factKey, factTexts) {
|
|
188
|
+
const texts = [...new Set((factTexts || []).filter(Boolean))];
|
|
189
|
+
if (!factKey || texts.length === 0) return { deletedLinks: 0, affectedDocuments: 0 };
|
|
190
|
+
const placeholders = texts.map(() => "?").join(",");
|
|
191
|
+
const links = await db
|
|
192
|
+
.prepare(`SELECT * FROM knowledge_links WHERE fact_key = ? AND fact_text IN (${placeholders})`)
|
|
193
|
+
.all(factKey, ...texts);
|
|
194
|
+
for (const link of links) {
|
|
195
|
+
const source = `fact:${factKey}:${link.fact_text.substring(0, 30)}`;
|
|
196
|
+
const target = link.start_line
|
|
197
|
+
? `${link.doc_id}:L${link.start_line}-${link.end_line || link.start_line}`
|
|
198
|
+
: link.doc_id;
|
|
199
|
+
await db
|
|
200
|
+
.prepare("DELETE FROM graph_edges WHERE source_id = ? AND target_id = ? AND relation_type = ?")
|
|
201
|
+
.run(source, target, link.relation_type || "LINKS_TO");
|
|
202
|
+
}
|
|
203
|
+
await db
|
|
204
|
+
.prepare(`DELETE FROM knowledge_links WHERE fact_key = ? AND fact_text IN (${placeholders})`)
|
|
205
|
+
.run(factKey, ...texts);
|
|
206
|
+
const docIds = [...new Set(links.map((link) => link.doc_id))];
|
|
207
|
+
for (const docId of docIds) await queueDocumentSyncIfNeeded(db, docId);
|
|
208
|
+
return { deletedLinks: links.length, affectedDocuments: docIds.length };
|
|
209
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getDatabase } from "../db/database.js";
|
|
2
2
|
import { writeFileSync, mkdirSync, existsSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { MEMORY_DIR } from "../memory.js";
|
|
4
|
+
import { MEMORY_DIR } from "../memory.js";
|
|
5
|
+
import { toVectorBytes } from "../retrieval/retriever.js";
|
|
5
6
|
|
|
6
7
|
export const EXPORTS_DIR = join(MEMORY_DIR, "exports");
|
|
7
8
|
|
|
@@ -35,8 +36,32 @@ export async function exportDocumentData(docIdOrPath, customDb = null) {
|
|
|
35
36
|
|
|
36
37
|
const sections = await db.prepare("SELECT id, heading, breadcrumbs, content, token_count FROM sections WHERE doc_id = ?").all(doc.id);
|
|
37
38
|
const mediumChunks = await db.prepare("SELECT id, section_id, content, block_type, token_count, created_at FROM medium_chunks WHERE doc_id = ?").all(doc.id);
|
|
38
|
-
const
|
|
39
|
-
const
|
|
39
|
+
const rawMicroChunks = await db.prepare("SELECT id, medium_id, section_id, content, vector, token_count, retrieval_policy, policy_source_id FROM micro_chunks WHERE doc_id = ?").all(doc.id);
|
|
40
|
+
const microChunks = rawMicroChunks.map((chunk) => {
|
|
41
|
+
const bytes = toVectorBytes(chunk.vector);
|
|
42
|
+
return {
|
|
43
|
+
...chunk,
|
|
44
|
+
vector: bytes && bytes.byteLength
|
|
45
|
+
? Buffer.from(bytes.buffer, bytes.byteOffset, bytes.byteLength).toString("base64")
|
|
46
|
+
: "",
|
|
47
|
+
};
|
|
48
|
+
});
|
|
49
|
+
const ownedRows = await db.prepare(`
|
|
50
|
+
SELECT id FROM sections WHERE doc_id = ?
|
|
51
|
+
UNION SELECT id FROM medium_chunks WHERE doc_id = ?
|
|
52
|
+
UNION SELECT id FROM micro_chunks WHERE doc_id = ?;
|
|
53
|
+
`).all(doc.id, doc.id, doc.id);
|
|
54
|
+
const ownedIds = [doc.id, ...ownedRows.map((row) => row.id)];
|
|
55
|
+
const placeholders = ownedIds.map(() => "?").join(",");
|
|
56
|
+
const graphEdges = await db.prepare(`
|
|
57
|
+
SELECT source_id, target_id, relation_type, metadata_json, created_at
|
|
58
|
+
FROM graph_edges
|
|
59
|
+
WHERE source_id IN (${placeholders})
|
|
60
|
+
OR target_id IN (${placeholders})
|
|
61
|
+
OR target_id GLOB ?;
|
|
62
|
+
`).all(...ownedIds, ...ownedIds, `${doc.id}:L*`);
|
|
63
|
+
const knowledgeLinks = await db.prepare("SELECT * FROM knowledge_links WHERE doc_id = ?").all(doc.id);
|
|
64
|
+
const documentScopes = await db.prepare("SELECT scope_key, created_at FROM document_scopes WHERE doc_id = ?").all(doc.id);
|
|
40
65
|
|
|
41
66
|
return {
|
|
42
67
|
document: {
|
|
@@ -44,9 +69,11 @@ export async function exportDocumentData(docIdOrPath, customDb = null) {
|
|
|
44
69
|
path: doc.path,
|
|
45
70
|
title: doc.title,
|
|
46
71
|
blob_hash: doc.blob_hash,
|
|
47
|
-
checksum: doc.checksum,
|
|
48
|
-
|
|
49
|
-
|
|
72
|
+
checksum: doc.checksum,
|
|
73
|
+
toc_json: doc.toc_json,
|
|
74
|
+
metadata_json: doc.metadata_json,
|
|
75
|
+
toc,
|
|
76
|
+
metadata,
|
|
50
77
|
created_at: doc.created_at,
|
|
51
78
|
updated_at: doc.updated_at,
|
|
52
79
|
},
|
|
@@ -59,9 +86,11 @@ export async function exportDocumentData(docIdOrPath, customDb = null) {
|
|
|
59
86
|
sections,
|
|
60
87
|
medium_chunks: mediumChunks,
|
|
61
88
|
micro_chunks: microChunks,
|
|
62
|
-
graph_edges: graphEdges,
|
|
63
|
-
|
|
64
|
-
|
|
89
|
+
graph_edges: graphEdges,
|
|
90
|
+
knowledge_links: knowledgeLinks,
|
|
91
|
+
document_scopes: documentScopes,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
65
94
|
|
|
66
95
|
export async function exportDocumentToJsonString(docIdOrPath, customDb = null) {
|
|
67
96
|
const data = await exportDocumentData(docIdOrPath, customDb);
|