context-vault 3.4.3 → 3.4.4

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.
Files changed (33) hide show
  1. package/dist/server.js +28 -10
  2. package/dist/server.js.map +1 -1
  3. package/dist/tools/get-context.d.ts +2 -1
  4. package/dist/tools/get-context.d.ts.map +1 -1
  5. package/dist/tools/get-context.js +22 -2
  6. package/dist/tools/get-context.js.map +1 -1
  7. package/dist/tools/save-context.d.ts +2 -1
  8. package/dist/tools/save-context.d.ts.map +1 -1
  9. package/dist/tools/save-context.js +63 -2
  10. package/dist/tools/save-context.js.map +1 -1
  11. package/node_modules/@context-vault/core/dist/context.d.ts +34 -0
  12. package/node_modules/@context-vault/core/dist/context.d.ts.map +1 -0
  13. package/node_modules/@context-vault/core/dist/context.js +55 -0
  14. package/node_modules/@context-vault/core/dist/context.js.map +1 -0
  15. package/node_modules/@context-vault/core/dist/db.d.ts +3 -1
  16. package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
  17. package/node_modules/@context-vault/core/dist/db.js +29 -2
  18. package/node_modules/@context-vault/core/dist/db.js.map +1 -1
  19. package/node_modules/@context-vault/core/dist/search.d.ts +1 -0
  20. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
  21. package/node_modules/@context-vault/core/dist/search.js +57 -3
  22. package/node_modules/@context-vault/core/dist/search.js.map +1 -1
  23. package/node_modules/@context-vault/core/dist/types.d.ts +6 -0
  24. package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
  25. package/node_modules/@context-vault/core/package.json +5 -1
  26. package/node_modules/@context-vault/core/src/context.ts +65 -0
  27. package/node_modules/@context-vault/core/src/db.ts +29 -2
  28. package/node_modules/@context-vault/core/src/search.ts +54 -2
  29. package/node_modules/@context-vault/core/src/types.ts +6 -0
  30. package/package.json +2 -2
  31. package/src/server.ts +32 -9
  32. package/src/tools/get-context.ts +25 -1
  33. package/src/tools/save-context.ts +62 -1
@@ -106,6 +106,7 @@ export async function hybridSearch(
106
106
  decayDays = 30,
107
107
  includeSuperseeded = false,
108
108
  includeEphemeral = false,
109
+ contextEmbedding = null,
109
110
  } = opts;
110
111
 
111
112
  const rowMap = new Map<string, VaultEntry>();
@@ -209,9 +210,60 @@ export async function hybridSearch(
209
210
  }
210
211
  }
211
212
 
213
+ // Context vector pass: KNN against vault_ctx_vec for contextual reinstatement
214
+ const ctxRankedIds: string[] = [];
215
+ if (contextEmbedding) {
216
+ try {
217
+ const ctxVecCount = (
218
+ ctx.db.prepare('SELECT COUNT(*) as c FROM vault_ctx_vec').get() as { c: number }
219
+ ).c;
220
+ if (ctxVecCount > 0) {
221
+ const ctxRows = ctx.db
222
+ .prepare(
223
+ `SELECT v.rowid, v.distance FROM vault_ctx_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT 15`
224
+ )
225
+ .all(contextEmbedding, 15) as { rowid: number; distance: number }[];
226
+
227
+ if (ctxRows.length) {
228
+ const ctxRowids = ctxRows.map((cr) => cr.rowid);
229
+ const placeholders = ctxRowids.map(() => '?').join(',');
230
+ const ctxHydrated = ctx.db
231
+ .prepare(`SELECT rowid, * FROM vault WHERE rowid IN (${placeholders})`)
232
+ .all(...ctxRowids) as unknown as (VaultEntry & { rowid: number })[];
233
+
234
+ const ctxByRowid = new Map<number, VaultEntry & { rowid: number }>();
235
+ for (const row of ctxHydrated) ctxByRowid.set(row.rowid, row);
236
+
237
+ for (const cr of ctxRows) {
238
+ const row = ctxByRowid.get(cr.rowid);
239
+ if (!row) continue;
240
+ if (kindFilter && row.kind !== kindFilter) continue;
241
+ if (categoryFilter && row.category !== categoryFilter) continue;
242
+ if (excludeEvents && row.category === 'event') continue;
243
+ if (since && row.created_at < since) continue;
244
+ if (until && row.created_at > until) continue;
245
+ if (row.expires_at && new Date(row.expires_at) <= new Date()) continue;
246
+
247
+ const { rowid: _rowid, ...cleanRow } = row;
248
+ ctxRankedIds.push(cleanRow.id);
249
+ if (!rowMap.has(cleanRow.id)) rowMap.set(cleanRow.id, cleanRow);
250
+ if (!idToRowid.has(cleanRow.id)) idToRowid.set(cleanRow.id, Number(row.rowid));
251
+ }
252
+ }
253
+ }
254
+ } catch (err) {
255
+ if (!(err as Error).message?.includes('no such table')) {
256
+ console.error(`[retrieve] Context vector search error: ${(err as Error).message}`);
257
+ }
258
+ }
259
+ }
260
+
212
261
  if (rowMap.size === 0) return [];
213
262
 
214
- const rrfScores = reciprocalRankFusion([ftsRankedIds, vecRankedIds]);
263
+ // Build ranked lists for RRF: content FTS + content vec + optional context vec
264
+ const rankedLists = [ftsRankedIds, vecRankedIds];
265
+ if (ctxRankedIds.length > 0) rankedLists.push(ctxRankedIds);
266
+ const rrfScores = reciprocalRankFusion(rankedLists);
215
267
 
216
268
  for (const [id, entry] of rowMap) {
217
269
  const boost = recencyBoost(entry.created_at, entry.category, decayDays);
@@ -274,7 +326,7 @@ export async function hybridSearch(
274
326
  return finalPage;
275
327
  }
276
328
 
277
- function trackAccess(ctx: BaseCtx, entries: SearchResult[]): void {
329
+ export function trackAccess(ctx: BaseCtx, entries: SearchResult[]): void {
278
330
  if (!entries.length) return;
279
331
  try {
280
332
  const placeholders = entries.map(() => '?').join(',');
@@ -53,6 +53,8 @@ export interface PreparedStatements {
53
53
  deleteVecStmt: StatementSync;
54
54
  updateSupersededBy: StatementSync;
55
55
  clearSupersededByRef: StatementSync;
56
+ insertCtxVecStmt: StatementSync;
57
+ deleteCtxVecStmt: StatementSync;
56
58
  }
57
59
 
58
60
  export interface VaultEntry {
@@ -151,6 +153,8 @@ export interface BaseCtx {
151
153
  embed: (text: string) => Promise<Float32Array | null>;
152
154
  insertVec: (rowid: number, embedding: Float32Array) => void;
153
155
  deleteVec: (rowid: number) => void;
156
+ insertCtxVec: (rowid: number, embedding: Float32Array) => void;
157
+ deleteCtxVec: (rowid: number) => void;
154
158
  }
155
159
 
156
160
  export interface SearchOptions {
@@ -164,4 +168,6 @@ export interface SearchOptions {
164
168
  decayDays?: number;
165
169
  includeSuperseeded?: boolean;
166
170
  includeEphemeral?: boolean;
171
+ /** Pre-computed context embedding for contextual reinstatement boosting. */
172
+ contextEmbedding?: Float32Array | null;
167
173
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "context-vault",
3
- "version": "3.4.3",
3
+ "version": "3.4.4",
4
4
  "type": "module",
5
5
  "description": "Persistent memory for AI agents — saves and searches knowledge across sessions",
6
6
  "bin": {
@@ -63,7 +63,7 @@
63
63
  "@context-vault/core"
64
64
  ],
65
65
  "dependencies": {
66
- "@context-vault/core": "^3.4.3",
66
+ "@context-vault/core": "^3.4.4",
67
67
  "@modelcontextprotocol/sdk": "^1.26.0",
68
68
  "adm-zip": "^0.5.16",
69
69
  "sqlite-vec": "^0.1.0"
package/src/server.ts CHANGED
@@ -26,6 +26,8 @@ import {
26
26
  prepareStatements,
27
27
  insertVec,
28
28
  deleteVec,
29
+ insertCtxVec,
30
+ deleteCtxVec,
29
31
  } from '@context-vault/core/db';
30
32
  import { registerTools } from './register-tools.js';
31
33
  import { pruneExpired } from '@context-vault/core/index';
@@ -328,6 +330,8 @@ async function main(): Promise<void> {
328
330
  embed,
329
331
  insertVec: (rowid: number, embedding: Float32Array) => insertVec(stmts, rowid, embedding),
330
332
  deleteVec: (rowid: number) => deleteVec(stmts, rowid),
333
+ insertCtxVec: (rowid: number, embedding: Float32Array) => insertCtxVec(stmts, rowid, embedding),
334
+ deleteCtxVec: (rowid: number) => deleteCtxVec(stmts, rowid),
331
335
  activeOps: { count: 0 },
332
336
  toolStats: { ok: 0, errors: 0, lastError: null },
333
337
  };
@@ -480,15 +484,34 @@ async function main(): Promise<void> {
480
484
  await transport.handleRequest(req, res, (req as any).body);
481
485
  return;
482
486
  } else if (sessionId) {
483
- // Stale session (e.g., daemon restarted). Per MCP spec, 404 tells
484
- // the client to re-initialize automatically.
485
- res.writeHead(404, { 'Content-Type': 'application/json' });
486
- res.end(JSON.stringify({
487
- jsonrpc: '2.0',
488
- error: { code: -32000, message: 'Session not found. Please reinitialize.' },
489
- id: null,
490
- }));
491
- return;
487
+ // Stale session (e.g., daemon restarted). Claude Code's MCP client
488
+ // does not auto-reinitialize on 404, so we recover transparently:
489
+ // create a new transport reusing the stale session ID, force it into
490
+ // initialized state, and handle the request as if nothing happened.
491
+ console.error(`[context-vault] Recovering stale session ${sessionId.slice(0, 8)}...`);
492
+
493
+ transport = new StreamableHTTPServerTransport({
494
+ sessionIdGenerator: () => sessionId,
495
+ onsessioninitialized: (sid: string) => {
496
+ transports[sid] = transport;
497
+ },
498
+ });
499
+ transport.onclose = () => {
500
+ if (transports[sessionId]) delete transports[sessionId];
501
+ };
502
+
503
+ const sessionServer = createServer();
504
+ await sessionServer.connect(transport);
505
+
506
+ // Force transport into initialized state, bypassing the initialize
507
+ // handshake. The inner WebStandardStreamableHTTPServerTransport holds
508
+ // the _initialized flag and sessionId.
509
+ const inner = (transport as any)._webStandardTransport;
510
+ inner._initialized = true;
511
+ inner.sessionId = sessionId;
512
+ transports[sessionId] = transport;
513
+
514
+ // Fall through to handleRequest below
492
515
  } else {
493
516
  res.writeHead(400, { 'Content-Type': 'application/json' });
494
517
  res.end(JSON.stringify({
@@ -2,9 +2,10 @@ import { z } from 'zod';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { readFileSync, existsSync } from 'node:fs';
4
4
  import { resolve } from 'node:path';
5
- import { hybridSearch } from '@context-vault/core/search';
5
+ import { hybridSearch, trackAccess } from '@context-vault/core/search';
6
6
  import { categoryFor } from '@context-vault/core/categories';
7
7
  import { normalizeKind } from '@context-vault/core/files';
8
+ import { parseContextParam } from '@context-vault/core/context';
8
9
  import { resolveTemporalParams } from '../temporal.js';
9
10
  import { collectLinkedEntries } from '../linking.js';
10
11
  import { ok, err, errWithHint } from '../helpers.js';
@@ -346,6 +347,12 @@ export const inputSchema = {
346
347
  .describe(
347
348
  'When true with identity_key, return a clear "not found" instead of falling through to semantic search on miss. Default: false.'
348
349
  ),
350
+ context: z
351
+ .any()
352
+ .optional()
353
+ .describe(
354
+ 'Current context for contextual reinstatement. Boosts entries that were saved in a similar context. Pass a structured object (e.g. { project: "myapp", arc: "auth-rewrite", task: "debugging token expiry" }) or a free-text string. Entries saved with matching encoding_context will rank higher.'
355
+ ),
349
356
  };
350
357
 
351
358
  export async function handler(
@@ -369,6 +376,7 @@ export async function handler(
369
376
  follow_links,
370
377
  body_limit,
371
378
  strict,
379
+ context,
372
380
  }: Record<string, any>,
373
381
  ctx: LocalCtx,
374
382
  { ensureIndexed, reindexFailed }: SharedCtx
@@ -413,6 +421,7 @@ export async function handler(
413
421
  if (!kindFilter) return err('identity_key requires kind to be specified', 'INVALID_INPUT');
414
422
  const match = ctx.stmts.getByIdentityKey.get(kindFilter, identity_key) as any;
415
423
  if (match) {
424
+ trackAccess(ctx, [{ ...match, score: 1 }]);
416
425
  const entryTags = match.tags ? JSON.parse(match.tags) : [];
417
426
  const tagStr = entryTags.length ? entryTags.join(', ') : 'none';
418
427
  const relPath =
@@ -452,6 +461,17 @@ export async function handler(
452
461
  ? Math.min(effectiveLimit * 10, MAX_FETCH_LIMIT)
453
462
  : effectiveLimit;
454
463
 
464
+ // Generate context embedding for contextual reinstatement boosting
465
+ let contextEmbedding: Float32Array | null = null;
466
+ const parsedCtx = parseContextParam(context);
467
+ if (parsedCtx?.text) {
468
+ try {
469
+ contextEmbedding = await ctx.embed(parsedCtx.text);
470
+ } catch {
471
+ // Non-fatal: proceed without context boosting
472
+ }
473
+ }
474
+
455
475
  let filtered: any[];
456
476
  if (hasQuery) {
457
477
  // Hybrid search mode
@@ -465,6 +485,7 @@ export async function handler(
465
485
  decayDays: config.eventDecayDays || 30,
466
486
  includeSuperseeded: include_superseded ?? false,
467
487
  includeEphemeral: include_ephemeral ?? false,
488
+ contextEmbedding,
468
489
  });
469
490
 
470
491
  // Post-filter by tags if provided, then apply requested limit
@@ -532,6 +553,9 @@ export async function handler(
532
553
 
533
554
  // Add score field for consistent output
534
555
  for (const r of filtered) r.score = 0;
556
+
557
+ // Track access for filter-only results (hybrid search tracks its own)
558
+ trackAccess(ctx, filtered);
535
559
  }
536
560
 
537
561
  // Brief score boost: briefs rank slightly higher so consolidated snapshots
@@ -3,6 +3,7 @@ import { captureAndIndex, updateEntryFile } from '@context-vault/core/capture';
3
3
  import { indexEntry } from '@context-vault/core/index';
4
4
  import { categoryFor, defaultTierFor } from '@context-vault/core/categories';
5
5
  import { normalizeKind } from '@context-vault/core/files';
6
+ import { parseContextParam } from '@context-vault/core/context';
6
7
  import { ok, err, errWithHint, ensureVaultExists, ensureValidKind } from '../helpers.js';
7
8
  import { maybeShowFeedbackPrompt } from '../telemetry.js';
8
9
  import { validateRelatedTo } from '../linking.js';
@@ -310,6 +311,12 @@ export const inputSchema = {
310
311
  .describe(
311
312
  'Conflict resolution mode. "suggest" (default): when similar entries are found, return structured conflict_candidates with suggested_action (ADD/UPDATE/SKIP) and reasoning_context for the calling agent to decide. Thresholds: score > 0.95 → SKIP (near-duplicate), score > 0.85 → UPDATE (very similar), score < 0.85 → ADD (distinct enough). "off": flag similar entries only (legacy behavior).'
312
313
  ),
314
+ encoding_context: z
315
+ .any()
316
+ .optional()
317
+ .describe(
318
+ 'Encoding context for contextual reinstatement. Captures the situation when this entry was created, enabling context-aware retrieval boosting. Pass a structured object (e.g. { project: "myapp", arc: "auth-rewrite", task: "implementing JWT" }) or a free-text string describing the current context.'
319
+ ),
313
320
  };
314
321
 
315
322
  export async function handler(
@@ -331,6 +338,7 @@ export async function handler(
331
338
  similarity_threshold,
332
339
  tier,
333
340
  conflict_resolution,
341
+ encoding_context,
334
342
  }: Record<string, any>,
335
343
  ctx: LocalCtx,
336
344
  { ensureIndexed }: SharedCtx
@@ -388,13 +396,21 @@ export async function handler(
388
396
  );
389
397
  }
390
398
 
399
+ // Merge encoding context into meta for update path
400
+ const updateParsedCtx = parseContextParam(encoding_context);
401
+ let updateMeta = meta;
402
+ if (updateParsedCtx) {
403
+ updateMeta = { ...(meta || {}) };
404
+ updateMeta.encoding_context = updateParsedCtx.structured || updateParsedCtx.text;
405
+ }
406
+
391
407
  let entry;
392
408
  try {
393
409
  entry = updateEntryFile(ctx, existing, {
394
410
  title,
395
411
  body,
396
412
  tags,
397
- meta,
413
+ meta: updateMeta,
398
414
  source,
399
415
  expires_at,
400
416
  supersedes,
@@ -409,6 +425,24 @@ export async function handler(
409
425
  'context-vault save_context update is failing. Check `cat ~/.context-mcp/error.log | tail -5` and help me debug.'
410
426
  );
411
427
  }
428
+
429
+ // Store context embedding for updated entry
430
+ if (updateParsedCtx?.text) {
431
+ try {
432
+ const ctxEmbed = await ctx.embed(updateParsedCtx.text);
433
+ if (ctxEmbed) {
434
+ const rowidResult = ctx.stmts.getRowid.get(entry.id) as { rowid: number } | undefined;
435
+ if (rowidResult?.rowid) {
436
+ const rowid = Number(rowidResult.rowid);
437
+ try { ctx.deleteCtxVec(rowid); } catch {}
438
+ ctx.insertCtxVec(rowid, ctxEmbed);
439
+ }
440
+ }
441
+ } catch (e) {
442
+ console.warn(`[context-vault] Context embedding update failed: ${(e as Error).message}`);
443
+ }
444
+ }
445
+
412
446
  if (entry.related_to?.length && ctx.stmts.updateRelatedTo) {
413
447
  ctx.stmts.updateRelatedTo.run(JSON.stringify(entry.related_to), entry.id);
414
448
  } else if (entry.related_to === null && ctx.stmts.updateRelatedTo) {
@@ -502,6 +536,15 @@ export async function handler(
502
536
 
503
537
  const mergedMeta = { ...(meta || {}) };
504
538
  if (folder) mergedMeta.folder = folder;
539
+
540
+ // Merge encoding context into meta for persistence
541
+ const parsedCtx = parseContextParam(encoding_context);
542
+ if (parsedCtx?.structured) {
543
+ mergedMeta.encoding_context = parsedCtx.structured;
544
+ } else if (parsedCtx?.text) {
545
+ mergedMeta.encoding_context = parsedCtx.text;
546
+ }
547
+
505
548
  const finalMeta = Object.keys(mergedMeta).length ? mergedMeta : undefined;
506
549
 
507
550
  const effectiveTier = tier ?? defaultTierFor(normalizedKind);
@@ -538,6 +581,24 @@ export async function handler(
538
581
  );
539
582
  }
540
583
 
584
+ // Store context embedding in vault_ctx_vec for contextual reinstatement
585
+ if (parsedCtx?.text && entry) {
586
+ try {
587
+ const ctxEmbedding = await ctx.embed(parsedCtx.text);
588
+ if (ctxEmbedding) {
589
+ const rowidResult = ctx.stmts.getRowid.get(entry.id) as { rowid: number } | undefined;
590
+ if (rowidResult?.rowid) {
591
+ const rowid = Number(rowidResult.rowid);
592
+ try { ctx.deleteCtxVec(rowid); } catch {}
593
+ ctx.insertCtxVec(rowid, ctxEmbedding);
594
+ }
595
+ }
596
+ } catch (e) {
597
+ // Non-fatal: context embedding failure should not block the save
598
+ console.warn(`[context-vault] Context embedding failed: ${(e as Error).message}`);
599
+ }
600
+ }
601
+
541
602
  if (ctx.config?.dataDir) {
542
603
  maybeShowFeedbackPrompt(ctx.config.dataDir);
543
604
  }