context-vault 2.8.13 → 2.8.14
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.
|
@@ -14,38 +14,51 @@ let extractor = null;
|
|
|
14
14
|
/** @type {null | true | false} null = unknown, true = working, false = failed */
|
|
15
15
|
let embedAvailable = null;
|
|
16
16
|
|
|
17
|
+
/** Shared promise for in-flight initialization — prevents concurrent loads */
|
|
18
|
+
let loadingPromise = null;
|
|
19
|
+
|
|
17
20
|
async function ensurePipeline() {
|
|
18
21
|
if (embedAvailable === false) return null;
|
|
19
22
|
if (extractor) return extractor;
|
|
23
|
+
if (loadingPromise) return loadingPromise;
|
|
20
24
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
25
|
+
loadingPromise = (async () => {
|
|
26
|
+
try {
|
|
27
|
+
// Dynamic import — @huggingface/transformers is optional (its transitive
|
|
28
|
+
// dep `sharp` can fail to install on some platforms). When missing, the
|
|
29
|
+
// server still works with full-text search only.
|
|
30
|
+
const { pipeline, env } = await import("@huggingface/transformers");
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
+
// Redirect model cache to ~/.context-mcp/models/ so it works when the
|
|
33
|
+
// package is installed globally in a root-owned directory (e.g. /usr/lib/node_modules/).
|
|
34
|
+
const modelCacheDir = join(homedir(), ".context-mcp", "models");
|
|
35
|
+
mkdirSync(modelCacheDir, { recursive: true });
|
|
36
|
+
env.cacheDir = modelCacheDir;
|
|
32
37
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
38
|
+
console.error(
|
|
39
|
+
"[context-vault] Loading embedding model (first run may download ~22MB)...",
|
|
40
|
+
);
|
|
41
|
+
extractor = await pipeline(
|
|
42
|
+
"feature-extraction",
|
|
43
|
+
"Xenova/all-MiniLM-L6-v2",
|
|
44
|
+
);
|
|
45
|
+
embedAvailable = true;
|
|
46
|
+
return extractor;
|
|
47
|
+
} catch (e) {
|
|
48
|
+
embedAvailable = false;
|
|
49
|
+
console.error(
|
|
50
|
+
`[context-vault] Failed to load embedding model: ${e.message}`,
|
|
51
|
+
);
|
|
52
|
+
console.error(
|
|
53
|
+
`[context-vault] Semantic search disabled. Full-text search still works.`,
|
|
54
|
+
);
|
|
55
|
+
return null;
|
|
56
|
+
} finally {
|
|
57
|
+
loadingPromise = null;
|
|
58
|
+
}
|
|
59
|
+
})();
|
|
60
|
+
|
|
61
|
+
return loadingPromise;
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
export async function embed(text) {
|
|
@@ -57,6 +70,7 @@ export async function embed(text) {
|
|
|
57
70
|
if (!result?.data?.length) {
|
|
58
71
|
extractor = null;
|
|
59
72
|
embedAvailable = null;
|
|
73
|
+
loadingPromise = null;
|
|
60
74
|
throw new Error("Embedding pipeline returned empty result");
|
|
61
75
|
}
|
|
62
76
|
return new Float32Array(result.data);
|
|
@@ -76,6 +90,7 @@ export async function embedBatch(texts) {
|
|
|
76
90
|
if (!result?.data?.length) {
|
|
77
91
|
extractor = null;
|
|
78
92
|
embedAvailable = null;
|
|
93
|
+
loadingPromise = null;
|
|
79
94
|
throw new Error("Embedding pipeline returned empty result");
|
|
80
95
|
}
|
|
81
96
|
const dim = result.data.length / texts.length;
|
|
@@ -93,6 +108,7 @@ export async function embedBatch(texts) {
|
|
|
93
108
|
export function resetEmbedPipeline() {
|
|
94
109
|
extractor = null;
|
|
95
110
|
embedAvailable = null;
|
|
111
|
+
loadingPromise = null;
|
|
96
112
|
}
|
|
97
113
|
|
|
98
114
|
/** Check if embedding is currently available. */
|
|
@@ -307,11 +307,16 @@ export async function reindex(ctx, opts = {}) {
|
|
|
307
307
|
created,
|
|
308
308
|
);
|
|
309
309
|
if (result.changes > 0) {
|
|
310
|
-
const
|
|
311
|
-
|
|
312
|
-
.
|
|
313
|
-
|
|
314
|
-
|
|
310
|
+
const rowidResult = ctx.stmts.getRowid.get(id);
|
|
311
|
+
if (rowidResult?.rowid) {
|
|
312
|
+
const embeddingText = [parsed.title, parsed.body]
|
|
313
|
+
.filter(Boolean)
|
|
314
|
+
.join(" ");
|
|
315
|
+
pendingEmbeds.push({
|
|
316
|
+
rowid: rowidResult.rowid,
|
|
317
|
+
text: embeddingText,
|
|
318
|
+
});
|
|
319
|
+
}
|
|
315
320
|
stats.added++;
|
|
316
321
|
} else {
|
|
317
322
|
stats.unchanged++;
|
|
@@ -157,9 +157,9 @@ export async function hybridSearch(
|
|
|
157
157
|
: 15;
|
|
158
158
|
const vecRows = ctx.db
|
|
159
159
|
.prepare(
|
|
160
|
-
`SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT
|
|
160
|
+
`SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT ?`,
|
|
161
161
|
)
|
|
162
|
-
.all(queryVec);
|
|
162
|
+
.all(queryVec, vecLimit);
|
|
163
163
|
|
|
164
164
|
if (vecRows.length) {
|
|
165
165
|
// Batch hydration: single query instead of N+1
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-vault",
|
|
3
|
-
"version": "2.8.
|
|
3
|
+
"version": "2.8.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
|
|
6
6
|
"bin": {
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"@context-vault/core"
|
|
56
56
|
],
|
|
57
57
|
"dependencies": {
|
|
58
|
-
"@context-vault/core": "^2.8.
|
|
58
|
+
"@context-vault/core": "^2.8.14",
|
|
59
59
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
60
60
|
"better-sqlite3": "^12.6.2",
|
|
61
61
|
"sqlite-vec": "^0.1.0"
|