context-vault 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/bin/cli.js +11 -2
- package/node_modules/@context-vault/core/package.json +1 -1
- package/node_modules/@context-vault/core/src/index/embed.js +5 -5
- package/node_modules/@context-vault/core/src/index/index.js +16 -13
- package/node_modules/@context-vault/core/src/server/helpers.js +1 -1
- package/node_modules/@context-vault/core/src/server/tools/delete-context.js +9 -2
- package/node_modules/@context-vault/core/src/server/tools/get-context.js +4 -1
- package/node_modules/@context-vault/core/src/server/tools.js +6 -1
- package/package.json +2 -2
package/bin/cli.js
CHANGED
|
@@ -1793,6 +1793,15 @@ async function runExport() {
|
|
|
1793
1793
|
}
|
|
1794
1794
|
}
|
|
1795
1795
|
|
|
1796
|
+
function safeJsonParse(str, fallback) {
|
|
1797
|
+
if (!str) return fallback;
|
|
1798
|
+
try {
|
|
1799
|
+
return JSON.parse(str);
|
|
1800
|
+
} catch {
|
|
1801
|
+
return fallback;
|
|
1802
|
+
}
|
|
1803
|
+
}
|
|
1804
|
+
|
|
1796
1805
|
function mapExportRow(row) {
|
|
1797
1806
|
return {
|
|
1798
1807
|
id: row.id,
|
|
@@ -1800,8 +1809,8 @@ function mapExportRow(row) {
|
|
|
1800
1809
|
category: row.category,
|
|
1801
1810
|
title: row.title || null,
|
|
1802
1811
|
body: row.body || null,
|
|
1803
|
-
tags:
|
|
1804
|
-
meta:
|
|
1812
|
+
tags: safeJsonParse(row.tags, []),
|
|
1813
|
+
meta: safeJsonParse(row.meta, {}),
|
|
1805
1814
|
source: row.source || null,
|
|
1806
1815
|
identity_key: row.identity_key || null,
|
|
1807
1816
|
expires_at: row.expires_at || null,
|
|
@@ -11,10 +11,10 @@ import { mkdirSync } from "node:fs";
|
|
|
11
11
|
|
|
12
12
|
let extractor = null;
|
|
13
13
|
|
|
14
|
-
/** @type {null | true | false} null =
|
|
14
|
+
/** @type {null | true | false} null = uninitialized/retry, true = ready, false = permanently failed */
|
|
15
15
|
let embedAvailable = null;
|
|
16
16
|
|
|
17
|
-
/**
|
|
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
|
-
|
|
103
|
-
|
|
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({
|
|
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
|
-
//
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "2.8.16",
|
|
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.16",
|
|
59
59
|
"@modelcontextprotocol/sdk": "^1.26.0",
|
|
60
60
|
"better-sqlite3": "^12.6.2",
|
|
61
61
|
"sqlite-vec": "^0.1.0"
|