@context-vault/core 2.8.14 → 2.8.16

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@context-vault/core",
3
- "version": "2.8.14",
3
+ "version": "2.8.16",
4
4
  "type": "module",
5
5
  "description": "Shared core: capture, index, retrieve, tools, and utilities for context-vault",
6
6
  "main": "src/index.js",
@@ -11,10 +11,10 @@ import { mkdirSync } from "node:fs";
11
11
 
12
12
  let extractor = null;
13
13
 
14
- /** @type {null | true | false} null = unknown, true = working, false = failed */
14
+ /** @type {null | true | false} null = uninitialized/retry, true = ready, false = permanently failed */
15
15
  let embedAvailable = null;
16
16
 
17
- /** Shared promise for in-flight initializationprevents concurrent loads */
17
+ /** In-flight load promise coalesces concurrent callers onto a single pipeline() call */
18
18
  let loadingPromise = null;
19
19
 
20
20
  async function ensurePipeline() {
@@ -99,9 +99,9 @@ export async function embedBatch(texts) {
99
99
  `Unexpected embedding dimension: ${result.data.length} / ${texts.length} = ${dim}`,
100
100
  );
101
101
  }
102
- return texts.map(
103
- (_, i) => new Float32Array(result.data.buffer, i * dim * 4, dim),
104
- );
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));
105
105
  }
106
106
 
107
107
  /** Force re-initialization on next embed call. */
@@ -49,6 +49,11 @@ export async function indexEntry(
49
49
  userId,
50
50
  },
51
51
  ) {
52
+ // Don't index entries that have already expired
53
+ if (expires_at && new Date(expires_at) <= new Date()) {
54
+ return;
55
+ }
56
+
52
57
  const tagsJson = tags ? JSON.stringify(tags) : null;
53
58
  const metaJson = meta ? JSON.stringify(meta) : null;
54
59
  const cat = category || categoryFor(kind);
@@ -239,8 +244,7 @@ export async function reindex(ctx, opts = {}) {
239
244
  // Phase 1: Sync DB ops in a transaction — FTS is searchable immediately after COMMIT.
240
245
  // Phase 2: Async embedding runs post-transaction so it can't hold the write lock
241
246
  // or roll back DB state on failure.
242
- const pendingEmbeds = []; // { rowid, text }
243
- const staleVecRowids = []; // rowids whose old vectors need deleting before re-embed
247
+ const pendingEmbeds = []; // { rowid, text, isUpdate }
244
248
 
245
249
  ctx.db.exec("BEGIN");
246
250
  try {
@@ -347,11 +351,14 @@ export async function reindex(ctx, opts = {}) {
347
351
  if (bodyChanged || titleChanged) {
348
352
  const rowid = ctx.stmts.getRowid.get(existing.id)?.rowid;
349
353
  if (rowid) {
350
- staleVecRowids.push(rowid);
351
354
  const embeddingText = [parsed.title, parsed.body]
352
355
  .filter(Boolean)
353
356
  .join(" ");
354
- pendingEmbeds.push({ rowid, text: embeddingText });
357
+ pendingEmbeds.push({
358
+ rowid,
359
+ text: embeddingText,
360
+ isUpdate: true,
361
+ });
355
362
  }
356
363
  }
357
364
  stats.updated++;
@@ -433,20 +440,16 @@ export async function reindex(ctx, opts = {}) {
433
440
 
434
441
  // Phase 2: Async embedding — runs after COMMIT so FTS is already searchable.
435
442
  // Failures here are non-fatal; semantic search catches up on next reindex.
436
-
437
- // Delete stale vectors for updated entries before re-embedding
438
- for (const rowid of staleVecRowids) {
439
- try {
440
- ctx.deleteVec(rowid);
441
- } catch {}
442
- }
443
-
444
- // Batch embed all pending texts
443
+ // Vec delete happens atomically with insert (only on success) to avoid
444
+ // leaving entries permanently without vectors if embedBatch() fails mid-batch.
445
445
  for (let i = 0; i < pendingEmbeds.length; i += EMBED_BATCH_SIZE) {
446
446
  const batch = pendingEmbeds.slice(i, i + EMBED_BATCH_SIZE);
447
447
  const embeddings = await embedBatch(batch.map((e) => e.text));
448
448
  for (let j = 0; j < batch.length; j++) {
449
449
  if (embeddings[j]) {
450
+ try {
451
+ ctx.deleteVec(batch[j].rowid);
452
+ } catch {}
450
453
  ctx.insertVec(batch[j].rowid, embeddings[j]);
451
454
  }
452
455
  }
@@ -13,7 +13,7 @@ export function err(text, code = "UNKNOWN") {
13
13
  export function ensureVaultExists(config) {
14
14
  if (!config.vaultDirExists) {
15
15
  return err(
16
- `Vault directory not found: ${config.vaultDir}. Run context_status for diagnostics.`,
16
+ `Vault directory not found: ${config.vaultDir}. Run context-status for diagnostics.`,
17
17
  "VAULT_NOT_FOUND",
18
18
  );
19
19
  }
@@ -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
  }
@@ -110,7 +110,10 @@ export async function handler(
110
110
 
111
111
  const effectiveLimit = limit || 10;
112
112
  // When tag-filtering, over-fetch to compensate for post-filter reduction
113
- const fetchLimit = tags?.length ? effectiveLimit * 10 : effectiveLimit;
113
+ const MAX_FETCH_LIMIT = 500;
114
+ const fetchLimit = tags?.length
115
+ ? Math.min(effectiveLimit * 10, MAX_FETCH_LIMIT)
116
+ : effectiveLimit;
114
117
 
115
118
  let filtered;
116
119
  if (hasQuery) {
@@ -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",