@lotargo/memory_plugin 1.1.4 → 1.1.6
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/README.md +243 -234
- package/mcp-server/admin/server.js +228 -228
- package/mcp-server/admin/snapshot.js +303 -303
- package/mcp-server/benchmarks/fetch_real_corpus.js +351 -351
- package/mcp-server/benchmarks/gpu_profile_benchmark.js +170 -0
- package/mcp-server/benchmarks/stress_ingestion.js +195 -193
- package/mcp-server/benchmarks/test_dual_layer.js +140 -140
- package/mcp-server/cli.js +293 -7
- package/mcp-server/config/config_manager.js +11 -7
- package/mcp-server/db/database.js +43 -43
- package/mcp-server/graph/graph_extractor.js +72 -72
- package/mcp-server/graph/knowledge_linker.js +102 -102
- package/mcp-server/index.js +454 -454
- package/mcp-server/ingest/chunker.js +337 -337
- package/mcp-server/ingest/exporter.js +80 -80
- package/mcp-server/ingest/normalizer.js +104 -104
- package/mcp-server/ingest/pipeline.js +22 -7
- package/mcp-server/ingest/sentence_segmenter.js +74 -74
- package/mcp-server/memory.js +72 -72
- package/mcp-server/ml/gpu_monitor.js +166 -0
- package/mcp-server/ml/model_manager.js +327 -17
- package/mcp-server/preinstall.js +44 -22
- package/mcp-server/retrieval/retriever.js +10 -5
- package/mcp-server/setup.js +148 -148
- package/mcp-server/storage/blob_store.js +62 -62
- package/opencode-plugin/index.js +244 -244
- package/package.json +58 -54
- package/skills/using-memory/SKILL.md +122 -122
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
import assert from "node:assert";
|
|
2
|
-
import { rmSync, existsSync } from "node:fs";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { tmpdir } from "node:os";
|
|
5
|
-
import { getDatabase } from "../db/database.js";
|
|
6
|
-
import { ingestDocument } from "../ingest/pipeline.js";
|
|
7
|
-
import { hybridQuery } from "../retrieval/retriever.js";
|
|
8
|
-
|
|
9
|
-
const PANEL_WIDTH = 58;
|
|
10
|
-
|
|
11
|
-
function printRichPanel(title, subtitle = "") {
|
|
12
|
-
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
13
|
-
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
14
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37m${title.padEnd(PANEL_WIDTH - 6)}\x1b[0m \x1b[36m│\x1b[0m`);
|
|
15
|
-
if (subtitle) {
|
|
16
|
-
console.log(`\x1b[36m│\x1b[0m \x1b[90m${subtitle.padEnd(PANEL_WIDTH - 6)}\x1b[0m \x1b[36m│\x1b[0m`);
|
|
17
|
-
}
|
|
18
|
-
console.log(`\x1b[36m╰${line}╯\x1b[0m`);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export async function testDualLayerArchitecture() {
|
|
22
|
-
printRichPanel("DUAL-LAYER VERIFICATION SUITE", "Layer 1: Notebook Facts vs Layer 2: RAG Engine");
|
|
23
|
-
|
|
24
|
-
const TEST_DIR = join(tmpdir(), `memory_test_dual_layer_${Date.now()}`);
|
|
25
|
-
const TEST_DB_PATH = join(TEST_DIR, "test_dual_layer.sqlite");
|
|
26
|
-
const TEST_BLOB_DIR = join(TEST_DIR, "blobs");
|
|
27
|
-
|
|
28
|
-
const results = {
|
|
29
|
-
notebookLayerPassed: false,
|
|
30
|
-
ragLayerPassed: false,
|
|
31
|
-
isolationPassed: false,
|
|
32
|
-
details: [],
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
try {
|
|
36
|
-
const db = getDatabase(TEST_DB_PATH);
|
|
37
|
-
|
|
38
|
-
// 1. Setup Layer 1: Persistent Personal Facts (Notebook Store)
|
|
39
|
-
console.log("\n 1. Testing Layer 1: Persistent Personal Facts (Notebook Store)...");
|
|
40
|
-
const personalFacts = [
|
|
41
|
-
"- [2026-07-30 02:30] User's name is Alex",
|
|
42
|
-
"- [2026-07-30 02:30] User prefers zero-Docker Node.js architecture with SQLite",
|
|
43
|
-
"- [2026-07-30 02:30] Project goal is building enterprise-grade local memory_plugin",
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
const readNotebookFacts = () => personalFacts.map((f) => f.slice(2));
|
|
47
|
-
const recalledFacts = readNotebookFacts();
|
|
48
|
-
|
|
49
|
-
assert.strictEqual(recalledFacts.length, 3, "Notebook facts should return all saved entries");
|
|
50
|
-
assert(recalledFacts[0].includes("User's name is Alex"), "Notebook fact 1 should contain user name");
|
|
51
|
-
console.log(" [PASS] Notebook Layer returns persistent user facts instantly with 100% precision.");
|
|
52
|
-
results.notebookLayerPassed = true;
|
|
53
|
-
results.details.push("Notebook Layer: 100% precision instant recall verified.");
|
|
54
|
-
|
|
55
|
-
// 2. Setup Layer 2: RAG Knowledge Base (Vector + BM25 + GraphRAG)
|
|
56
|
-
console.log("\n 2. Testing Layer 2: RAG Knowledge Base (Vector + BM25 Search)...");
|
|
57
|
-
const doc1 = `
|
|
58
|
-
# React Architecture Guide
|
|
59
|
-
React is a JavaScript library for building user interfaces.
|
|
60
|
-
Components render JSX and use state hooks like useState and useEffect.
|
|
61
|
-
`;
|
|
62
|
-
const doc2 = `
|
|
63
|
-
# SQLite Database Manual
|
|
64
|
-
SQLite is a C-language library that implements a small, fast, self-contained SQL database engine.
|
|
65
|
-
It supports Full-Text Search FTS5 and Write-Ahead Logging WAL mode.
|
|
66
|
-
`;
|
|
67
|
-
|
|
68
|
-
const ingestRes1 = await ingestDocument({
|
|
69
|
-
content: doc1,
|
|
70
|
-
type: "text",
|
|
71
|
-
title: "React Architecture Guide",
|
|
72
|
-
path: "docs/react.md",
|
|
73
|
-
customDb: db,
|
|
74
|
-
customBlobDir: TEST_BLOB_DIR,
|
|
75
|
-
generateEmbeddings: false,
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
const ingestRes2 = await ingestDocument({
|
|
79
|
-
content: doc2,
|
|
80
|
-
type: "text",
|
|
81
|
-
title: "SQLite Database Manual",
|
|
82
|
-
path: "docs/sqlite.md",
|
|
83
|
-
customDb: db,
|
|
84
|
-
customBlobDir: TEST_BLOB_DIR,
|
|
85
|
-
generateEmbeddings: false,
|
|
86
|
-
});
|
|
87
|
-
|
|
88
|
-
assert(ingestRes1.docId && ingestRes2.docId, "RAG documents should be ingested successfully");
|
|
89
|
-
console.log(" [PASS] RAG Knowledge Base ingestion OK.");
|
|
90
|
-
|
|
91
|
-
const ragResults = await hybridQuery({
|
|
92
|
-
query: "SQLite FTS5 full-text search engine",
|
|
93
|
-
limit: 5,
|
|
94
|
-
generateEmbeddings: false,
|
|
95
|
-
customDb: db,
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
assert(ragResults.length > 0, "RAG query should return matching knowledge sections");
|
|
99
|
-
assert(ragResults[0].doc_title.includes("SQLite"), "Top result should match query context");
|
|
100
|
-
console.log(" [PASS] RAG Knowledge Base returns dynamically retrieved doc section.");
|
|
101
|
-
results.ragLayerPassed = true;
|
|
102
|
-
results.details.push("RAG Layer: Dynamic hybrid retrieval verified.");
|
|
103
|
-
|
|
104
|
-
// 3. Test Architectural Isolation
|
|
105
|
-
console.log("\n 3. Testing Architectural Isolation between Notebook & RAG...");
|
|
106
|
-
|
|
107
|
-
const docsInDb = db.prepare("SELECT COUNT(*) as cnt FROM documents").get().cnt;
|
|
108
|
-
assert.strictEqual(docsInDb, 2, "SQLite DB should contain exactly 2 ingested documents, 0 notebook facts");
|
|
109
|
-
|
|
110
|
-
const emptyRagResults = await hybridQuery({
|
|
111
|
-
query: "NonExistentTopicForSearch12345",
|
|
112
|
-
limit: 5,
|
|
113
|
-
generateEmbeddings: false,
|
|
114
|
-
customDb: db,
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
assert.strictEqual(emptyRagResults.length, 0, "RAG query for non-existent topic should return empty list without leaking notebook facts");
|
|
118
|
-
console.log(" [PASS] Zero cross-contamination between Notebook Store and RAG Database.");
|
|
119
|
-
results.isolationPassed = true;
|
|
120
|
-
results.details.push("Isolation: 100% separation verified.");
|
|
121
|
-
|
|
122
|
-
db.close();
|
|
123
|
-
return results;
|
|
124
|
-
} catch (err) {
|
|
125
|
-
console.error(" [FAIL] Dual-Layer Test Failed:", err);
|
|
126
|
-
throw err;
|
|
127
|
-
} finally {
|
|
128
|
-
if (existsSync(TEST_DIR)) {
|
|
129
|
-
try {
|
|
130
|
-
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
131
|
-
} catch {
|
|
132
|
-
// Ignore temp lock on Windows
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
if (process.argv[1] && process.argv[1].includes("test_dual_layer.js")) {
|
|
139
|
-
await testDualLayerArchitecture();
|
|
140
|
-
}
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { rmSync, existsSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { getDatabase } from "../db/database.js";
|
|
6
|
+
import { ingestDocument } from "../ingest/pipeline.js";
|
|
7
|
+
import { hybridQuery } from "../retrieval/retriever.js";
|
|
8
|
+
|
|
9
|
+
const PANEL_WIDTH = 58;
|
|
10
|
+
|
|
11
|
+
function printRichPanel(title, subtitle = "") {
|
|
12
|
+
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
13
|
+
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
14
|
+
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37m${title.padEnd(PANEL_WIDTH - 6)}\x1b[0m \x1b[36m│\x1b[0m`);
|
|
15
|
+
if (subtitle) {
|
|
16
|
+
console.log(`\x1b[36m│\x1b[0m \x1b[90m${subtitle.padEnd(PANEL_WIDTH - 6)}\x1b[0m \x1b[36m│\x1b[0m`);
|
|
17
|
+
}
|
|
18
|
+
console.log(`\x1b[36m╰${line}╯\x1b[0m`);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export async function testDualLayerArchitecture() {
|
|
22
|
+
printRichPanel("DUAL-LAYER VERIFICATION SUITE", "Layer 1: Notebook Facts vs Layer 2: RAG Engine");
|
|
23
|
+
|
|
24
|
+
const TEST_DIR = join(tmpdir(), `memory_test_dual_layer_${Date.now()}`);
|
|
25
|
+
const TEST_DB_PATH = join(TEST_DIR, "test_dual_layer.sqlite");
|
|
26
|
+
const TEST_BLOB_DIR = join(TEST_DIR, "blobs");
|
|
27
|
+
|
|
28
|
+
const results = {
|
|
29
|
+
notebookLayerPassed: false,
|
|
30
|
+
ragLayerPassed: false,
|
|
31
|
+
isolationPassed: false,
|
|
32
|
+
details: [],
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
try {
|
|
36
|
+
const db = getDatabase(TEST_DB_PATH);
|
|
37
|
+
|
|
38
|
+
// 1. Setup Layer 1: Persistent Personal Facts (Notebook Store)
|
|
39
|
+
console.log("\n 1. Testing Layer 1: Persistent Personal Facts (Notebook Store)...");
|
|
40
|
+
const personalFacts = [
|
|
41
|
+
"- [2026-07-30 02:30] User's name is Alex",
|
|
42
|
+
"- [2026-07-30 02:30] User prefers zero-Docker Node.js architecture with SQLite",
|
|
43
|
+
"- [2026-07-30 02:30] Project goal is building enterprise-grade local memory_plugin",
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
const readNotebookFacts = () => personalFacts.map((f) => f.slice(2));
|
|
47
|
+
const recalledFacts = readNotebookFacts();
|
|
48
|
+
|
|
49
|
+
assert.strictEqual(recalledFacts.length, 3, "Notebook facts should return all saved entries");
|
|
50
|
+
assert(recalledFacts[0].includes("User's name is Alex"), "Notebook fact 1 should contain user name");
|
|
51
|
+
console.log(" [PASS] Notebook Layer returns persistent user facts instantly with 100% precision.");
|
|
52
|
+
results.notebookLayerPassed = true;
|
|
53
|
+
results.details.push("Notebook Layer: 100% precision instant recall verified.");
|
|
54
|
+
|
|
55
|
+
// 2. Setup Layer 2: RAG Knowledge Base (Vector + BM25 + GraphRAG)
|
|
56
|
+
console.log("\n 2. Testing Layer 2: RAG Knowledge Base (Vector + BM25 Search)...");
|
|
57
|
+
const doc1 = `
|
|
58
|
+
# React Architecture Guide
|
|
59
|
+
React is a JavaScript library for building user interfaces.
|
|
60
|
+
Components render JSX and use state hooks like useState and useEffect.
|
|
61
|
+
`;
|
|
62
|
+
const doc2 = `
|
|
63
|
+
# SQLite Database Manual
|
|
64
|
+
SQLite is a C-language library that implements a small, fast, self-contained SQL database engine.
|
|
65
|
+
It supports Full-Text Search FTS5 and Write-Ahead Logging WAL mode.
|
|
66
|
+
`;
|
|
67
|
+
|
|
68
|
+
const ingestRes1 = await ingestDocument({
|
|
69
|
+
content: doc1,
|
|
70
|
+
type: "text",
|
|
71
|
+
title: "React Architecture Guide",
|
|
72
|
+
path: "docs/react.md",
|
|
73
|
+
customDb: db,
|
|
74
|
+
customBlobDir: TEST_BLOB_DIR,
|
|
75
|
+
generateEmbeddings: false,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const ingestRes2 = await ingestDocument({
|
|
79
|
+
content: doc2,
|
|
80
|
+
type: "text",
|
|
81
|
+
title: "SQLite Database Manual",
|
|
82
|
+
path: "docs/sqlite.md",
|
|
83
|
+
customDb: db,
|
|
84
|
+
customBlobDir: TEST_BLOB_DIR,
|
|
85
|
+
generateEmbeddings: false,
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
assert(ingestRes1.docId && ingestRes2.docId, "RAG documents should be ingested successfully");
|
|
89
|
+
console.log(" [PASS] RAG Knowledge Base ingestion OK.");
|
|
90
|
+
|
|
91
|
+
const ragResults = await hybridQuery({
|
|
92
|
+
query: "SQLite FTS5 full-text search engine",
|
|
93
|
+
limit: 5,
|
|
94
|
+
generateEmbeddings: false,
|
|
95
|
+
customDb: db,
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
assert(ragResults.length > 0, "RAG query should return matching knowledge sections");
|
|
99
|
+
assert(ragResults[0].doc_title.includes("SQLite"), "Top result should match query context");
|
|
100
|
+
console.log(" [PASS] RAG Knowledge Base returns dynamically retrieved doc section.");
|
|
101
|
+
results.ragLayerPassed = true;
|
|
102
|
+
results.details.push("RAG Layer: Dynamic hybrid retrieval verified.");
|
|
103
|
+
|
|
104
|
+
// 3. Test Architectural Isolation
|
|
105
|
+
console.log("\n 3. Testing Architectural Isolation between Notebook & RAG...");
|
|
106
|
+
|
|
107
|
+
const docsInDb = db.prepare("SELECT COUNT(*) as cnt FROM documents").get().cnt;
|
|
108
|
+
assert.strictEqual(docsInDb, 2, "SQLite DB should contain exactly 2 ingested documents, 0 notebook facts");
|
|
109
|
+
|
|
110
|
+
const emptyRagResults = await hybridQuery({
|
|
111
|
+
query: "NonExistentTopicForSearch12345",
|
|
112
|
+
limit: 5,
|
|
113
|
+
generateEmbeddings: false,
|
|
114
|
+
customDb: db,
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
assert.strictEqual(emptyRagResults.length, 0, "RAG query for non-existent topic should return empty list without leaking notebook facts");
|
|
118
|
+
console.log(" [PASS] Zero cross-contamination between Notebook Store and RAG Database.");
|
|
119
|
+
results.isolationPassed = true;
|
|
120
|
+
results.details.push("Isolation: 100% separation verified.");
|
|
121
|
+
|
|
122
|
+
db.close();
|
|
123
|
+
return results;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
console.error(" [FAIL] Dual-Layer Test Failed:", err);
|
|
126
|
+
throw err;
|
|
127
|
+
} finally {
|
|
128
|
+
if (existsSync(TEST_DIR)) {
|
|
129
|
+
try {
|
|
130
|
+
rmSync(TEST_DIR, { recursive: true, force: true });
|
|
131
|
+
} catch {
|
|
132
|
+
// Ignore temp lock on Windows
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
if (process.argv[1] && process.argv[1].includes("test_dual_layer.js")) {
|
|
139
|
+
await testDualLayerArchitecture();
|
|
140
|
+
}
|
package/mcp-server/cli.js
CHANGED
|
@@ -8,20 +8,28 @@ import { deleteDocument } from "./ingest/pipeline.js";
|
|
|
8
8
|
import { readMemoryRaw, readMemory, writeMemory, GLOBAL_KEY, projectName } from "./memory.js";
|
|
9
9
|
import { getCorpusCacheSize, clearCorpusCache } from "./benchmarks/fetch_real_corpus.js";
|
|
10
10
|
import { SMOKE_DOC_IDS } from "./benchmarks/quality_evaluator.js";
|
|
11
|
+
import { getModelStorageInfo, deleteModelCache, listAllCachedModels } from "./ml/model_manager.js";
|
|
11
12
|
|
|
12
13
|
const EMBEDDING_PRESETS = [
|
|
13
14
|
"Xenova/multilingual-e5-small",
|
|
15
|
+
"Xenova/multilingual-e5-base",
|
|
14
16
|
"Xenova/multilingual-e5-large",
|
|
17
|
+
"Xenova/bge-small-en-v1.5",
|
|
18
|
+
"Xenova/bge-base-en-v1.5",
|
|
19
|
+
"Xenova/bge-large-en-v1.5",
|
|
15
20
|
"Xenova/bge-m3",
|
|
16
21
|
"Xenova/all-MiniLM-L6-v2",
|
|
17
|
-
"Xenova/
|
|
22
|
+
"Xenova/all-mpnet-base-v2",
|
|
18
23
|
"Xenova/paraphrase-multilingual-MiniLM-L12-v2",
|
|
24
|
+
"Xenova/gte-small",
|
|
25
|
+
"Xenova/gte-large",
|
|
19
26
|
];
|
|
20
27
|
|
|
21
28
|
const RERANKER_PRESETS = [
|
|
22
29
|
"none",
|
|
23
30
|
"Xenova/bge-reranker-base",
|
|
24
|
-
"Xenova/bge-reranker-
|
|
31
|
+
"Xenova/bge-reranker-large",
|
|
32
|
+
"Xenova/ms-marco-MiniLM-L-6-v2",
|
|
25
33
|
"Xenova/ms-marco-TinyBERT-L-2-v2",
|
|
26
34
|
];
|
|
27
35
|
|
|
@@ -650,7 +658,7 @@ export async function runCli() {
|
|
|
650
658
|
label: "Embedding Model",
|
|
651
659
|
badge: config.embeddingModel.split("/").pop(),
|
|
652
660
|
value: "embedding",
|
|
653
|
-
info: `Model: ${config.embeddingModel}. ONNX Feature Extraction via @
|
|
661
|
+
info: `Model: ${config.embeddingModel}. ONNX Feature Extraction via @huggingface/transformers`,
|
|
654
662
|
},
|
|
655
663
|
{
|
|
656
664
|
label: "Reranker Model",
|
|
@@ -658,6 +666,32 @@ export async function runCli() {
|
|
|
658
666
|
value: "reranker",
|
|
659
667
|
info: config.rerankerEnabled ? `Reranker active: ${config.rerankerModel}` : "Optional Cross-Encoder re-ranking pass",
|
|
660
668
|
},
|
|
669
|
+
{
|
|
670
|
+
label: "Vector Batch Size",
|
|
671
|
+
badge: `${config.batchSize || 12} Chunks`,
|
|
672
|
+
value: "batch_size",
|
|
673
|
+
info: `Ingestion batch size: ${config.batchSize || 12} micro-chunks per ONNX pass`,
|
|
674
|
+
},
|
|
675
|
+
{
|
|
676
|
+
label: "GPU Attention Budget",
|
|
677
|
+
badge: `${((config.gpuAttentionBudget || 2000000) / 1000000).toFixed(1)}M Units`,
|
|
678
|
+
value: "gpu_budget",
|
|
679
|
+
info: `Micro-batch tensor budget: ${((config.gpuAttentionBudget || 2000000) / 1000000).toFixed(1)}M quadratic units (controls max peak VRAM usage on GPU)`,
|
|
680
|
+
},
|
|
681
|
+
{
|
|
682
|
+
label: "CPU WASM Threads",
|
|
683
|
+
badge: config.onnxThreads > 0 ? `${config.onnxThreads} Threads` : "AUTO (CPU Cores)",
|
|
684
|
+
value: "onnx_threads",
|
|
685
|
+
info: config.onnxThreads > 0 ? `ONNX execution threads manually set to ${config.onnxThreads}` : "Auto-detect optimal physical CPU threads",
|
|
686
|
+
},
|
|
687
|
+
{
|
|
688
|
+
label: "Execution Hardware",
|
|
689
|
+
badge: (config.executionDevice || "cpu").toUpperCase() === "WEBGPU" || (config.executionDevice || "cpu").toUpperCase() === "GPU" ? "\x1b[31mGPU (EXPERIMENTAL)\x1b[0m" : "CPU (AVX2)",
|
|
690
|
+
value: "execution_device",
|
|
691
|
+
info: config.executionDevice === "webgpu" || config.executionDevice === "gpu"
|
|
692
|
+
? "⚠️ EXPERIMENTAL: ONNX DirectML GPU execution (high VRAM/padding overhead, CPU AVX2 recommended)"
|
|
693
|
+
: "CPU inference via AVX2 / WASM SIMD (Recommended for stability & speed)",
|
|
694
|
+
},
|
|
661
695
|
],
|
|
662
696
|
},
|
|
663
697
|
{
|
|
@@ -685,6 +719,11 @@ export async function runCli() {
|
|
|
685
719
|
value: "import_snapshot",
|
|
686
720
|
info: "Import RAG database, vectors & blobs from a snapshot file (.json or .json.gz)",
|
|
687
721
|
},
|
|
722
|
+
{
|
|
723
|
+
label: "[MODELS] Manage & Purge ML Model Cache",
|
|
724
|
+
value: "manage_models",
|
|
725
|
+
info: "Inspect cached ONNX models on disk, check status (Ready / Partial / Not Downloaded) & delete models to free disk space",
|
|
726
|
+
},
|
|
688
727
|
{
|
|
689
728
|
label: "[HARD RESET] Purge RAG Base & Blob Storage",
|
|
690
729
|
value: "hard_reset",
|
|
@@ -769,13 +808,19 @@ export async function runCli() {
|
|
|
769
808
|
break;
|
|
770
809
|
}
|
|
771
810
|
case "embedding": {
|
|
772
|
-
const embItems = EMBEDDING_PRESETS.map((m) =>
|
|
811
|
+
const embItems = EMBEDDING_PRESETS.map((m) => {
|
|
812
|
+
const info = getModelStorageInfo(m);
|
|
813
|
+
let badge = "NOT DOWNLOADED";
|
|
814
|
+
if (info.status === "downloaded") badge = `READY (${info.sizeMB} MB)`;
|
|
815
|
+
else if (info.status === "partial") badge = `INCOMPLETE (${info.sizeMB} MB)`;
|
|
816
|
+
return { label: m, badge, value: m, info: `Model: ${m} [${badge}]` };
|
|
817
|
+
});
|
|
773
818
|
embItems.push({ label: "Custom HuggingFace Model...", value: "custom", info: "Specify custom HF model string" });
|
|
774
819
|
const initialEmbIdx = Math.max(0, embItems.findIndex((i) => i.value === config.embeddingModel));
|
|
775
820
|
|
|
776
821
|
const subRes = await selectSimpleMenu({
|
|
777
822
|
title: "SELECT EMBEDDING MODEL",
|
|
778
|
-
subtitle: "Dense vector extraction model via @
|
|
823
|
+
subtitle: "Dense vector extraction model via @huggingface/transformers",
|
|
779
824
|
items: embItems,
|
|
780
825
|
initialIndex: initialEmbIdx,
|
|
781
826
|
});
|
|
@@ -799,7 +844,13 @@ export async function runCli() {
|
|
|
799
844
|
case "reranker": {
|
|
800
845
|
const rkItems = [
|
|
801
846
|
{ label: "Disable Reranker", value: "none", info: "No cross-encoder re-ranking" },
|
|
802
|
-
...RERANKER_PRESETS.filter((r) => r !== "none").map((r) =>
|
|
847
|
+
...RERANKER_PRESETS.filter((r) => r !== "none").map((r) => {
|
|
848
|
+
const info = getModelStorageInfo(r);
|
|
849
|
+
let badge = "NOT DOWNLOADED";
|
|
850
|
+
if (info.status === "downloaded") badge = `READY (${info.sizeMB} MB)`;
|
|
851
|
+
else if (info.status === "partial") badge = `INCOMPLETE (${info.sizeMB} MB)`;
|
|
852
|
+
return { label: r, badge, value: r, info: `Reranker: ${r} [${badge}]` };
|
|
853
|
+
}),
|
|
803
854
|
{ label: "Custom Reranker Model...", value: "custom", info: "Specify custom HuggingFace cross-encoder model" },
|
|
804
855
|
];
|
|
805
856
|
const currentRk = config.rerankerEnabled ? config.rerankerModel : "none";
|
|
@@ -832,6 +883,101 @@ export async function runCli() {
|
|
|
832
883
|
}
|
|
833
884
|
break;
|
|
834
885
|
}
|
|
886
|
+
case "batch_size": {
|
|
887
|
+
const batchItems = [
|
|
888
|
+
{ label: "Batch Size 1 (Single Item)", value: 1, info: "Process micro-chunks strictly 1 by 1" },
|
|
889
|
+
{ label: "Batch Size 4", value: 4, info: "Small CPU batch size" },
|
|
890
|
+
{ label: "Batch Size 8 (CPU Sweet Spot)", value: 8, info: "Optimal for CPU L3 cache" },
|
|
891
|
+
{ label: "Batch Size 12 (Default)", value: 12, info: "Balanced CPU throughput" },
|
|
892
|
+
{ label: "Batch Size 16", value: 16, info: "High throughput batch size" },
|
|
893
|
+
{ label: "Batch Size 32 (Standard GPU)", value: 32, info: "Standard GPU batching" },
|
|
894
|
+
{ label: "Batch Size 48 (High GPU)", value: 48, info: "High throughput GPU batching" },
|
|
895
|
+
{ label: "Batch Size 64 (Ultra GPU)", value: 64, info: "Ultra-fast GPU parallel tensor execution" },
|
|
896
|
+
{ label: "Batch Size 128 (Extreme GPU)", value: 128, info: "Massive GPU parallelism" },
|
|
897
|
+
{ label: "Batch Size 256 (Max GPU)", value: 256, info: "Maximum batch capacity for dedicated VRAM" },
|
|
898
|
+
];
|
|
899
|
+
const currentBatch = config.batchSize || 12;
|
|
900
|
+
const initialBatchIdx = Math.max(0, batchItems.findIndex((i) => i.value === currentBatch));
|
|
901
|
+
const subRes = await selectSimpleMenu({
|
|
902
|
+
title: "SELECT VECTOR BATCH SIZE",
|
|
903
|
+
subtitle: "Number of micro-chunks vectorized per ONNX inference pass",
|
|
904
|
+
items: batchItems,
|
|
905
|
+
initialIndex: initialBatchIdx,
|
|
906
|
+
});
|
|
907
|
+
if (subRes.action === "select") {
|
|
908
|
+
updateConfig({ batchSize: subRes.value });
|
|
909
|
+
}
|
|
910
|
+
break;
|
|
911
|
+
}
|
|
912
|
+
case "gpu_budget": {
|
|
913
|
+
const budgetItems = [
|
|
914
|
+
{ label: "1.0M Units (Conservative ~0.8 GB VRAM)", value: 1000000, info: "Ultra-safe for 4GB-6GB GPUs or heavy background multitasking" },
|
|
915
|
+
{ label: "2.0M Units (Balanced ~1.5 GB VRAM - Default)", value: 2000000, info: "Optimal balance between GPU throughput & safe VRAM ceiling" },
|
|
916
|
+
{ label: "4.0M Units (Aggressive ~2.5 GB VRAM)", value: 4000000, info: "Higher GPU parallel compute for dedicated 8GB+ GPUs" },
|
|
917
|
+
{ label: "8.0M Units (High Parallelism ~4.5 GB VRAM)", value: 8000000, info: "Maximum batching throughput for 12GB-16GB VRAM GPUs" },
|
|
918
|
+
{ label: "16.0M Units (Extreme ~8.0 GB VRAM)", value: 16000000, info: "Uncapped micro-batching for 24GB+ VRAM workstation GPUs" },
|
|
919
|
+
];
|
|
920
|
+
const currentBudget = config.gpuAttentionBudget || 2000000;
|
|
921
|
+
const initialIdx = Math.max(0, budgetItems.findIndex((i) => i.value === currentBudget));
|
|
922
|
+
const subRes = await selectSimpleMenu({
|
|
923
|
+
title: "SELECT GPU MICRO-BATCH ATTENTION BUDGET",
|
|
924
|
+
subtitle: "Controls dynamic O(seq_len^2) sub-batching to prevent VRAM overflow",
|
|
925
|
+
items: budgetItems,
|
|
926
|
+
initialIndex: initialIdx,
|
|
927
|
+
});
|
|
928
|
+
if (subRes.action === "select") {
|
|
929
|
+
updateConfig({ gpuAttentionBudget: subRes.value });
|
|
930
|
+
}
|
|
931
|
+
break;
|
|
932
|
+
}
|
|
933
|
+
case "onnx_threads": {
|
|
934
|
+
const threadItems = [
|
|
935
|
+
{ label: "0 - Auto (Detect CPU Cores)", value: 0, info: "Automatically match physical CPU cores (up to 8)" },
|
|
936
|
+
{ label: "1 Thread (Single-Threaded)", value: 1, info: "Restrict ONNX WASM to 1 thread" },
|
|
937
|
+
{ label: "2 Threads", value: 2, info: "Use 2 WASM threads" },
|
|
938
|
+
{ label: "4 Threads", value: 4, info: "Use 4 WASM threads" },
|
|
939
|
+
{ label: "8 Threads", value: 8, info: "Use 8 WASM threads" },
|
|
940
|
+
{ label: "16 Threads", value: 16, info: "Use 16 WASM threads" },
|
|
941
|
+
];
|
|
942
|
+
const currentThreads = config.onnxThreads || 0;
|
|
943
|
+
const initialThreadIdx = Math.max(0, threadItems.findIndex((i) => i.value === currentThreads));
|
|
944
|
+
const subRes = await selectSimpleMenu({
|
|
945
|
+
title: "SELECT CPU ONNX WASM THREADS",
|
|
946
|
+
subtitle: "Number of WASM worker threads for ONNX Runtime",
|
|
947
|
+
items: threadItems,
|
|
948
|
+
initialIndex: initialThreadIdx,
|
|
949
|
+
});
|
|
950
|
+
if (subRes.action === "select") {
|
|
951
|
+
updateConfig({ onnxThreads: subRes.value });
|
|
952
|
+
}
|
|
953
|
+
break;
|
|
954
|
+
}
|
|
955
|
+
case "execution_device": {
|
|
956
|
+
const devItems = [
|
|
957
|
+
{
|
|
958
|
+
label: "CPU (AVX2 / WASM SIMD - RECOMMENDED)",
|
|
959
|
+
value: "cpu",
|
|
960
|
+
info: "Standard multi-threaded CPU execution via ONNX native AVX2 (Optimal speed, stability & zero VRAM overhead)",
|
|
961
|
+
},
|
|
962
|
+
{
|
|
963
|
+
label: "\x1b[31m[EXPERIMENTAL]\x1b[0m GPU (DirectML / WebGPU)",
|
|
964
|
+
value: "webgpu",
|
|
965
|
+
info: "⚠️ EXPERIMENTAL: DirectML GPU tensor execution. High JS FFI & zero-padding overhead; CPU AVX2 is recommended for local Node.js.",
|
|
966
|
+
},
|
|
967
|
+
];
|
|
968
|
+
const currentDev = config.executionDevice || "cpu";
|
|
969
|
+
const initialDevIdx = Math.max(0, devItems.findIndex((i) => i.value === currentDev));
|
|
970
|
+
const subRes = await selectSimpleMenu({
|
|
971
|
+
title: "SELECT EXECUTION HARDWARE DEVICE",
|
|
972
|
+
subtitle: "CPU AVX2 (Recommended) vs Experimental DirectML GPU Hardware Mode",
|
|
973
|
+
items: devItems,
|
|
974
|
+
initialIndex: initialDevIdx,
|
|
975
|
+
});
|
|
976
|
+
if (subRes.action === "select") {
|
|
977
|
+
updateConfig({ executionDevice: subRes.value });
|
|
978
|
+
}
|
|
979
|
+
break;
|
|
980
|
+
}
|
|
835
981
|
case "notebook": {
|
|
836
982
|
let nbRunning = true;
|
|
837
983
|
while (nbRunning) {
|
|
@@ -1117,6 +1263,79 @@ export async function runCli() {
|
|
|
1117
1263
|
}
|
|
1118
1264
|
break;
|
|
1119
1265
|
}
|
|
1266
|
+
case "manage_models": {
|
|
1267
|
+
let modelMgmtRunning = true;
|
|
1268
|
+
while (modelMgmtRunning) {
|
|
1269
|
+
const allPresets = [...new Set([...EMBEDDING_PRESETS, ...RERANKER_PRESETS.filter((r) => r !== "none")])];
|
|
1270
|
+
const cachedOnDisk = listAllCachedModels();
|
|
1271
|
+
const diskModelNames = cachedOnDisk.map((m) => m.modelName);
|
|
1272
|
+
|
|
1273
|
+
const combinedModels = [...new Set([...allPresets, ...diskModelNames])];
|
|
1274
|
+
|
|
1275
|
+
let totalDiskBytes = 0;
|
|
1276
|
+
const modelItems = combinedModels.map((m) => {
|
|
1277
|
+
const info = getModelStorageInfo(m);
|
|
1278
|
+
totalDiskBytes += info.bytes;
|
|
1279
|
+
let badge = "NOT DOWNLOADED";
|
|
1280
|
+
if (info.status === "downloaded") badge = `READY (${info.sizeMB} MB)`;
|
|
1281
|
+
else if (info.status === "partial") badge = `INCOMPLETE (${info.sizeMB} MB)`;
|
|
1282
|
+
|
|
1283
|
+
return {
|
|
1284
|
+
label: m,
|
|
1285
|
+
badge,
|
|
1286
|
+
value: m,
|
|
1287
|
+
info: info.status !== "not_downloaded"
|
|
1288
|
+
? `Size: ${info.sizeMB} MB | Select to inspect or delete from disk`
|
|
1289
|
+
: "Model weights not present on local disk",
|
|
1290
|
+
};
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1293
|
+
modelItems.push({ label: "< Back to Main Menu", value: "back" });
|
|
1294
|
+
|
|
1295
|
+
const totalDiskMB = (totalDiskBytes / (1024 * 1024)).toFixed(2);
|
|
1296
|
+
const subRes = await selectSimpleMenu({
|
|
1297
|
+
title: "ML MODEL CACHE MANAGEMENT",
|
|
1298
|
+
subtitle: `Total ML Storage Used: ${totalDiskMB} MB | Models Tracked: ${combinedModels.length}`,
|
|
1299
|
+
items: modelItems,
|
|
1300
|
+
});
|
|
1301
|
+
|
|
1302
|
+
if (subRes.action === "back" || subRes.value === "back") {
|
|
1303
|
+
modelMgmtRunning = false;
|
|
1304
|
+
break;
|
|
1305
|
+
}
|
|
1306
|
+
|
|
1307
|
+
const selectedModel = subRes.value;
|
|
1308
|
+
const selectedInfo = getModelStorageInfo(selectedModel);
|
|
1309
|
+
|
|
1310
|
+
if (selectedInfo.status === "not_downloaded") {
|
|
1311
|
+
console.clear();
|
|
1312
|
+
console.log(`\n [*] Model "${selectedModel}" is not downloaded on local disk.\n`);
|
|
1313
|
+
await waitForEnter();
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const actionRes = await selectSimpleMenu({
|
|
1318
|
+
title: `MODEL ACTION: ${selectedModel}`,
|
|
1319
|
+
subtitle: `Status: ${selectedInfo.status.toUpperCase()} | Size: ${selectedInfo.sizeMB} MB`,
|
|
1320
|
+
items: [
|
|
1321
|
+
{ label: `[PURGE] Delete model weights from disk (${selectedInfo.sizeMB} MB)`, value: "delete", info: `Delete ${selectedInfo.dir} permanently` },
|
|
1322
|
+
{ label: "< Cancel / Back", value: "cancel" },
|
|
1323
|
+
],
|
|
1324
|
+
});
|
|
1325
|
+
|
|
1326
|
+
if (actionRes.action === "select" && actionRes.value === "delete") {
|
|
1327
|
+
const delRes = deleteModelCache(selectedModel);
|
|
1328
|
+
console.clear();
|
|
1329
|
+
if (delRes.deleted) {
|
|
1330
|
+
console.log(`\n \x1b[32m[OK] Model "${selectedModel}" deleted successfully (${delRes.freedMB} MB freed).\x1b[0m\n`);
|
|
1331
|
+
} else {
|
|
1332
|
+
console.error(`\n \x1b[31m[ERROR] Failed to delete model: ${delRes.reason}\x1b[0m\n`);
|
|
1333
|
+
}
|
|
1334
|
+
await waitForEnter();
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
break;
|
|
1338
|
+
}
|
|
1120
1339
|
case "benchmark": {
|
|
1121
1340
|
const modeRes = await selectSimpleMenu({
|
|
1122
1341
|
title: "BENCHMARK MODE",
|
|
@@ -1132,6 +1351,16 @@ export async function runCli() {
|
|
|
1132
1351
|
value: "full",
|
|
1133
1352
|
info: "Full 28-doc corpus, per-query answer token metrics, bootstrap CIs, grid sweep. Writes dev_docs/benchmark_results.md.",
|
|
1134
1353
|
},
|
|
1354
|
+
{
|
|
1355
|
+
label: "[GPU PROFILER] GPU Inference Bottleneck Trace",
|
|
1356
|
+
value: "gpu_profile",
|
|
1357
|
+
info: "Profile GPU DirectML tensor execution stages, kernel launch overhead & VRAM throughput.",
|
|
1358
|
+
},
|
|
1359
|
+
{
|
|
1360
|
+
label: "[CPU vs GPU] Dual-Run Comparison Benchmark",
|
|
1361
|
+
value: "cpu_vs_gpu",
|
|
1362
|
+
info: "Run identical workload on CPU then GPU and compare throughput, latency & speedup.",
|
|
1363
|
+
},
|
|
1135
1364
|
{
|
|
1136
1365
|
label: "Graph & Notebook Linking Verification (Layer 1+3 Agent Graph Links)",
|
|
1137
1366
|
value: "graph_test",
|
|
@@ -1211,6 +1440,56 @@ export async function runCli() {
|
|
|
1211
1440
|
break;
|
|
1212
1441
|
}
|
|
1213
1442
|
|
|
1443
|
+
if (modeRes.value === "gpu_profile") {
|
|
1444
|
+
console.clear();
|
|
1445
|
+
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
1446
|
+
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
1447
|
+
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37mGPU PROFILER BENCHMARK\x1b[0m${" ".repeat(PANEL_WIDTH - 28)}\x1b[36m│\x1b[0m`);
|
|
1448
|
+
console.log(`\x1b[36m│\x1b[0m \x1b[90m${"Profiling DirectML tensor execution stages & VRAM throughput".padEnd(PANEL_WIDTH - 6)}\x1b[0m \x1b[36m│\x1b[0m`);
|
|
1449
|
+
console.log(`\x1b[36m╰${line}╯\x1b[0m\n`);
|
|
1450
|
+
|
|
1451
|
+
const savedConfig = getConfig();
|
|
1452
|
+
try {
|
|
1453
|
+
const { runGpuProfileBenchmark } = await import("./benchmarks/gpu_profile_benchmark.js");
|
|
1454
|
+
await runGpuProfileBenchmark({
|
|
1455
|
+
modelName: savedConfig.embeddingModel,
|
|
1456
|
+
batchSize: savedConfig.batchSize || 32,
|
|
1457
|
+
totalItems: 512,
|
|
1458
|
+
});
|
|
1459
|
+
} catch (err) {
|
|
1460
|
+
console.error(` \x1b[31m[ERROR] GPU Profile benchmark failed: ${err.message}\x1b[0m\n`);
|
|
1461
|
+
}
|
|
1462
|
+
// Restore original device config
|
|
1463
|
+
updateConfig({ executionDevice: savedConfig.executionDevice });
|
|
1464
|
+
await waitForEnter();
|
|
1465
|
+
break;
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
if (modeRes.value === "cpu_vs_gpu") {
|
|
1469
|
+
console.clear();
|
|
1470
|
+
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
1471
|
+
console.log(`\x1b[36m╭${line}╮\x1b[0m`);
|
|
1472
|
+
console.log(`\x1b[36m│\x1b[0m \x1b[1m\x1b[37mCPU vs GPU COMPARISON BENCHMARK\x1b[0m${" ".repeat(PANEL_WIDTH - 37)}\x1b[36m│\x1b[0m`);
|
|
1473
|
+
console.log(`\x1b[36m│\x1b[0m \x1b[90m${"Identical workload on CPU then GPU — automatic device switching".padEnd(PANEL_WIDTH - 6)}\x1b[0m \x1b[36m│\x1b[0m`);
|
|
1474
|
+
console.log(`\x1b[36m╰${line}╯\x1b[0m\n`);
|
|
1475
|
+
|
|
1476
|
+
const savedConfig = getConfig();
|
|
1477
|
+
try {
|
|
1478
|
+
const { runCpuVsGpuComparison } = await import("./benchmarks/gpu_profile_benchmark.js");
|
|
1479
|
+
await runCpuVsGpuComparison({
|
|
1480
|
+
modelName: savedConfig.embeddingModel,
|
|
1481
|
+
batchSize: savedConfig.batchSize || 32,
|
|
1482
|
+
totalItems: 512,
|
|
1483
|
+
});
|
|
1484
|
+
} catch (err) {
|
|
1485
|
+
console.error(` \x1b[31m[ERROR] CPU vs GPU benchmark failed: ${err.message}\x1b[0m\n`);
|
|
1486
|
+
}
|
|
1487
|
+
// Restore original device config
|
|
1488
|
+
updateConfig({ executionDevice: savedConfig.executionDevice });
|
|
1489
|
+
await waitForEnter();
|
|
1490
|
+
break;
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1214
1493
|
const isSmoke = modeRes.value === "smoke";
|
|
1215
1494
|
console.clear();
|
|
1216
1495
|
const line = "─".repeat(PANEL_WIDTH - 2);
|
|
@@ -1329,5 +1608,12 @@ export async function runCli() {
|
|
|
1329
1608
|
}
|
|
1330
1609
|
|
|
1331
1610
|
if (process.argv[1] && process.argv[1].includes("cli.js")) {
|
|
1332
|
-
|
|
1611
|
+
if (typeof global.gc !== "function") {
|
|
1612
|
+
const { spawn } = await import("node:child_process");
|
|
1613
|
+
const args = ["--expose-gc", ...process.argv.slice(1)];
|
|
1614
|
+
const child = spawn(process.execPath, args, { stdio: "inherit" });
|
|
1615
|
+
child.on("exit", (code) => process.exit(code));
|
|
1616
|
+
} else {
|
|
1617
|
+
runCli().catch((err) => console.error("CLI error:", err));
|
|
1618
|
+
}
|
|
1333
1619
|
}
|