context-vault 2.8.13 → 2.8.15

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "2.8.13",
3
+ "version": "2.8.15",
4
4
  "type": "module",
5
5
  "description": "Shared core: capture, index, retrieve, tools, and utilities for context-vault",
6
6
  "main": "src/index.js",
@@ -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
- try {
22
- // Dynamic import — @huggingface/transformers is optional (its transitive
23
- // dep `sharp` can fail to install on some platforms). When missing, the
24
- // server still works with full-text search only.
25
- const { pipeline, env } = await import("@huggingface/transformers");
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
- // Redirect model cache to ~/.context-mcp/models/ so it works when the
28
- // package is installed globally in a root-owned directory (e.g. /usr/lib/node_modules/).
29
- const modelCacheDir = join(homedir(), ".context-mcp", "models");
30
- mkdirSync(modelCacheDir, { recursive: true });
31
- env.cacheDir = modelCacheDir;
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
- console.error(
34
- "[context-vault] Loading embedding model (first run may download ~22MB)...",
35
- );
36
- extractor = await pipeline("feature-extraction", "Xenova/all-MiniLM-L6-v2");
37
- embedAvailable = true;
38
- return extractor;
39
- } catch (e) {
40
- embedAvailable = false;
41
- console.error(
42
- `[context-vault] Failed to load embedding model: ${e.message}`,
43
- );
44
- console.error(
45
- `[context-vault] Semantic search disabled. Full-text search still works.`,
46
- );
47
- return null;
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;
@@ -84,15 +99,16 @@ export async function embedBatch(texts) {
84
99
  `Unexpected embedding dimension: ${result.data.length} / ${texts.length} = ${dim}`,
85
100
  );
86
101
  }
87
- return texts.map(
88
- (_, i) => new Float32Array(result.data.buffer, i * dim * 4, dim),
89
- );
102
+ // subarray() creates a view into result.data's index-space, correctly
103
+ // accounting for any non-zero byteOffset on the source typed array.
104
+ return texts.map((_, i) => result.data.subarray(i * dim, (i + 1) * dim));
90
105
  }
91
106
 
92
107
  /** Force re-initialization on next embed call. */
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 rowid = ctx.stmts.getRowid.get(id).rowid;
311
- const embeddingText = [parsed.title, parsed.body]
312
- .filter(Boolean)
313
- .join(" ");
314
- pendingEmbeds.push({ rowid, text: embeddingText });
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 ${vecLimit}`,
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
@@ -32,10 +32,16 @@ export async function handler({ id }, ctx, { ensureIndexed }) {
32
32
  }
33
33
 
34
34
  // Delete file from disk first (source of truth)
35
+ let fileWarning = null;
35
36
  if (entry.file_path) {
36
37
  try {
37
38
  unlinkSync(entry.file_path);
38
- } catch {}
39
+ } catch (e) {
40
+ // ENOENT = already gone — not an error worth surfacing
41
+ if (e.code !== "ENOENT") {
42
+ fileWarning = `file could not be removed from disk (${e.code}): ${entry.file_path}`;
43
+ }
44
+ }
39
45
  }
40
46
 
41
47
  // Delete vector embedding
@@ -49,5 +55,6 @@ export async function handler({ id }, ctx, { ensureIndexed }) {
49
55
  // Delete DB row (FTS trigger handles FTS cleanup)
50
56
  ctx.stmts.deleteEntry.run(id);
51
57
 
52
- return ok(`Deleted ${entry.kind}: ${entry.title || "(untitled)"} [${id}]`);
58
+ const msg = `Deleted ${entry.kind}: ${entry.title || "(untitled)"} [${id}]`;
59
+ return ok(fileWarning ? `${msg}\nWarning: ${fileWarning}` : msg);
53
60
  }
@@ -28,9 +28,11 @@ export function registerTools(server, ctx) {
28
28
  return async (...args) => {
29
29
  if (ctx.activeOps) ctx.activeOps.count++;
30
30
  let timer;
31
+ let handlerPromise;
31
32
  try {
33
+ handlerPromise = Promise.resolve(handler(...args));
32
34
  return await Promise.race([
33
- Promise.resolve(handler(...args)),
35
+ handlerPromise,
34
36
  new Promise((_, reject) => {
35
37
  timer = setTimeout(
36
38
  () => reject(new Error("TOOL_TIMEOUT")),
@@ -40,6 +42,9 @@ export function registerTools(server, ctx) {
40
42
  ]);
41
43
  } catch (e) {
42
44
  if (e.message === "TOOL_TIMEOUT") {
45
+ // Suppress any late rejection from the still-running handler to
46
+ // prevent unhandled promise rejection warnings in the host process.
47
+ handlerPromise?.catch(() => {});
43
48
  return err(
44
49
  "Tool timed out after 60s. Try a simpler query or run `context-vault reindex` first.",
45
50
  "TIMEOUT",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-vault",
3
- "version": "2.8.13",
3
+ "version": "2.8.15",
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.13",
58
+ "@context-vault/core": "^2.8.15",
59
59
  "@modelcontextprotocol/sdk": "^1.26.0",
60
60
  "better-sqlite3": "^12.6.2",
61
61
  "sqlite-vec": "^0.1.0"