context-vault 3.18.0 → 3.19.0

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 (62) hide show
  1. package/bin/cli.js +157 -0
  2. package/dist/register-tools.d.ts.map +1 -1
  3. package/dist/register-tools.js +0 -2
  4. package/dist/register-tools.js.map +1 -1
  5. package/dist/server.js +78 -1
  6. package/dist/server.js.map +1 -1
  7. package/dist/tools/recall.d.ts +1 -1
  8. package/dist/tools/recall.d.ts.map +1 -1
  9. package/dist/tools/recall.js +50 -100
  10. package/dist/tools/recall.js.map +1 -1
  11. package/node_modules/@context-vault/core/dist/assemble.d.ts +22 -0
  12. package/node_modules/@context-vault/core/dist/assemble.d.ts.map +1 -0
  13. package/node_modules/@context-vault/core/dist/assemble.js +143 -0
  14. package/node_modules/@context-vault/core/dist/assemble.js.map +1 -0
  15. package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
  16. package/node_modules/@context-vault/core/dist/capture.js +10 -5
  17. package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
  18. package/node_modules/@context-vault/core/dist/consolidation.d.ts +40 -0
  19. package/node_modules/@context-vault/core/dist/consolidation.d.ts.map +1 -0
  20. package/node_modules/@context-vault/core/dist/consolidation.js +229 -0
  21. package/node_modules/@context-vault/core/dist/consolidation.js.map +1 -0
  22. package/node_modules/@context-vault/core/dist/db.d.ts +25 -1
  23. package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
  24. package/node_modules/@context-vault/core/dist/db.js +92 -4
  25. package/node_modules/@context-vault/core/dist/db.js.map +1 -1
  26. package/node_modules/@context-vault/core/dist/frontmatter.d.ts.map +1 -1
  27. package/node_modules/@context-vault/core/dist/frontmatter.js +26 -3
  28. package/node_modules/@context-vault/core/dist/frontmatter.js.map +1 -1
  29. package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -1
  30. package/node_modules/@context-vault/core/dist/index.js +225 -184
  31. package/node_modules/@context-vault/core/dist/index.js.map +1 -1
  32. package/node_modules/@context-vault/core/dist/main.d.ts +2 -0
  33. package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
  34. package/node_modules/@context-vault/core/dist/main.js +2 -0
  35. package/node_modules/@context-vault/core/dist/main.js.map +1 -1
  36. package/node_modules/@context-vault/core/dist/search.d.ts +5 -0
  37. package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
  38. package/node_modules/@context-vault/core/dist/search.js +97 -5
  39. package/node_modules/@context-vault/core/dist/search.js.map +1 -1
  40. package/node_modules/@context-vault/core/dist/summarize.d.ts +5 -0
  41. package/node_modules/@context-vault/core/dist/summarize.d.ts.map +1 -0
  42. package/node_modules/@context-vault/core/dist/summarize.js +146 -0
  43. package/node_modules/@context-vault/core/dist/summarize.js.map +1 -0
  44. package/node_modules/@context-vault/core/dist/types.d.ts +2 -0
  45. package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
  46. package/node_modules/@context-vault/core/package.json +5 -1
  47. package/node_modules/@context-vault/core/src/assemble.ts +187 -0
  48. package/node_modules/@context-vault/core/src/capture.ts +10 -5
  49. package/node_modules/@context-vault/core/src/consolidation.ts +356 -0
  50. package/node_modules/@context-vault/core/src/db.ts +95 -4
  51. package/node_modules/@context-vault/core/src/frontmatter.ts +25 -4
  52. package/node_modules/@context-vault/core/src/index.ts +127 -88
  53. package/node_modules/@context-vault/core/src/main.ts +4 -0
  54. package/node_modules/@context-vault/core/src/search.ts +102 -5
  55. package/node_modules/@context-vault/core/src/summarize.ts +157 -0
  56. package/node_modules/@context-vault/core/src/types.ts +2 -0
  57. package/package.json +2 -2
  58. package/scripts/validate-epipe-shutdown.mjs +183 -0
  59. package/scripts/validate-sqlite-busy-retry.mjs +243 -0
  60. package/src/register-tools.ts +0 -2
  61. package/src/server.ts +76 -1
  62. package/src/tools/recall.ts +51 -110
@@ -1,11 +1,11 @@
1
1
  import { z } from 'zod';
2
2
  import { ok } from '../helpers.js';
3
- import { isEmbedAvailable } from '@context-vault/core/embed';
3
+ import { hybridSearch } from '@context-vault/core/search';
4
4
  import { getAutoMemory, findAutoMemoryOverlaps } from '../auto-memory.js';
5
5
  import { getRemoteClient, getTeamId, getPublicVaults } from '../remote.js';
6
6
  import type { LocalCtx, SharedCtx, ToolResult } from '../types.js';
7
+ import type { SearchOptions } from '@context-vault/core/types';
7
8
 
8
- const SEMANTIC_SIMILARITY_THRESHOLD = 0.6;
9
9
  const CO_RETRIEVAL_WEIGHT_CAP = 50;
10
10
 
11
11
  const STOPWORDS = new Set([
@@ -31,14 +31,14 @@ const sessionSurfaced = new Map<string, Set<string>>();
31
31
 
32
32
  /**
33
33
  * Extract keywords from a signal string.
34
- * Split on whitespace, filter stopwords and words under 4 chars, keep top 10.
34
+ * Split on whitespace, filter stopwords and words under 2 chars, keep top 10.
35
35
  */
36
36
  export function extractKeywords(signal: string): string[] {
37
37
  const words = signal
38
38
  .toLowerCase()
39
39
  .replace(/[^a-z0-9\s_-]/g, ' ')
40
40
  .split(/\s+/)
41
- .filter((w) => w.length >= 4 && !STOPWORDS.has(w));
41
+ .filter((w) => w.length >= 2 && !STOPWORDS.has(w));
42
42
 
43
43
  const seen = new Set<string>();
44
44
  const unique: string[] = [];
@@ -100,30 +100,48 @@ export async function handler(
100
100
  return result;
101
101
  }
102
102
 
103
- // Build fast-path query: tag/title LIKE match for each keyword
104
- const conditions: string[] = [];
105
- const params: string[] = [];
106
- for (const kw of keywords) {
107
- conditions.push('(title LIKE ? OR tags LIKE ?)');
108
- params.push(`%${kw}%`, `%${kw}%`);
109
- }
110
-
111
- const bucketClause = bucket ? ' AND tags LIKE ?' : '';
112
- if (bucket) params.push(`%"bucket:${bucket}"%`);
103
+ // Build search query from signal, enriched by signal_type
104
+ let searchQuery = signal || '';
105
+ const searchOpts: SearchOptions = {
106
+ excludeEvents: true,
107
+ limit: limit * 3, // over-fetch to allow for session dedup + bucket filtering
108
+ };
113
109
 
114
- const sql = `SELECT id, title, substr(body, 1, 100) as summary, kind, tags
115
- FROM vault
116
- WHERE indexed = 1
117
- AND (expires_at IS NULL OR expires_at > datetime('now'))
118
- AND superseded_by IS NULL
119
- AND (${conditions.join(' OR ')})${bucketClause}
120
- LIMIT 20`;
110
+ // Signal-type aware search options
111
+ switch (signal_type) {
112
+ case 'error':
113
+ // Errors should boost recent entries
114
+ searchOpts.decayDays = 7;
115
+ break;
116
+ case 'file':
117
+ // Extract path components and extension as additional search terms
118
+ searchQuery = signal
119
+ .replace(/[/\\]/g, ' ')
120
+ .replace(/\./g, ' ')
121
+ .trim();
122
+ break;
123
+ case 'task':
124
+ // Tasks benefit from wider search
125
+ searchOpts.limit = Math.max(searchOpts.limit!, limit * 5);
126
+ break;
127
+ // 'prompt': standard hybrid search, no modifications
128
+ }
121
129
 
122
- let rows: any[];
123
- try {
124
- rows = ctx.db.prepare(sql).all(...params);
125
- } catch {
126
- rows = [];
130
+ // Run hybrid search (FTS + vector + tag lanes with RRF fusion)
131
+ let searchResults = await hybridSearch(ctx, searchQuery, searchOpts);
132
+
133
+ // Bucket-aware post-filtering
134
+ if (bucket) {
135
+ const bucketTag = `bucket:${bucket}`;
136
+ searchResults = searchResults.filter((r) => {
137
+ if (!r.tags) return false;
138
+ try {
139
+ const tags: string[] = typeof r.tags === 'string' ? JSON.parse(r.tags) : r.tags;
140
+ return tags.some((t) => t === bucketTag);
141
+ } catch {
142
+ return String(r.tags).includes(bucketTag);
143
+ }
144
+ });
127
145
  }
128
146
 
129
147
  // Session dedup
@@ -142,7 +160,7 @@ export async function handler(
142
160
  tags: string[];
143
161
  }> = [];
144
162
 
145
- for (const row of rows) {
163
+ for (const row of searchResults) {
146
164
  if (hints.length >= limit) break;
147
165
 
148
166
  // Dedup check
@@ -151,21 +169,16 @@ export async function handler(
151
169
  continue;
152
170
  }
153
171
 
154
- const entryTags: string[] = row.tags ? JSON.parse(row.tags) : [];
172
+ const entryTags: string[] = row.tags
173
+ ? (typeof row.tags === 'string' ? JSON.parse(row.tags) : row.tags)
174
+ : [];
155
175
 
156
- // Score relevance: count how many keywords match title or tags
157
- let matchCount = 0;
158
- const titleLower = (row.title || '').toLowerCase();
159
- const tagsLower = (row.tags || '').toLowerCase();
160
- for (const kw of keywords) {
161
- if (titleLower.includes(kw) || tagsLower.includes(kw)) matchCount++;
162
- }
163
- const relevance: 'high' | 'medium' = matchCount >= 2 ? 'high' : 'medium';
176
+ const relevance: 'high' | 'medium' = row.score >= 0.02 ? 'high' : 'medium';
164
177
 
165
178
  hints.push({
166
179
  id: row.id,
167
180
  title: row.title || '(untitled)',
168
- summary: row.summary || '',
181
+ summary: row.body ? row.body.slice(0, 100) : '',
169
182
  relevance,
170
183
  kind: row.kind || 'knowledge',
171
184
  tags: entryTags,
@@ -261,79 +274,7 @@ export async function handler(
261
274
  }
262
275
  }
263
276
 
264
- let method: 'tag_match' | 'semantic' | 'none' = hints.length > 0 ? 'tag_match' : 'none';
265
-
266
- // Semantic fallback: when fast-path returns 0 results and signal is not file-based
267
- if (hints.length === 0 && signal_type !== 'file' && isEmbedAvailable()) {
268
- try {
269
- const vecCount = (
270
- ctx.db.prepare('SELECT COUNT(*) as c FROM vault_vec').get() as { c: number }
271
- ).c;
272
-
273
- if (vecCount > 0) {
274
- const queryVec = await ctx.embed(signal);
275
- if (queryVec) {
276
- const vecRows = ctx.db
277
- .prepare(
278
- 'SELECT v.rowid, v.distance FROM vault_vec v WHERE embedding MATCH ? ORDER BY distance LIMIT 5'
279
- )
280
- .all(queryVec) as { rowid: number; distance: number }[];
281
-
282
- if (vecRows.length) {
283
- const rowids = vecRows.map((vr) => vr.rowid);
284
- const placeholders = rowids.map(() => '?').join(',');
285
-
286
- let bucketFilter = '';
287
- const hydrateParams: any[] = [...rowids];
288
- if (bucket) {
289
- bucketFilter = ' AND tags LIKE ?';
290
- hydrateParams.push(`%"bucket:${bucket}"%`);
291
- }
292
-
293
- const hydrated = ctx.db
294
- .prepare(
295
- `SELECT rowid, id, title, substr(body, 1, 100) as summary, kind, tags FROM vault WHERE rowid IN (${placeholders}) AND indexed = 1 AND (expires_at IS NULL OR expires_at > datetime('now')) AND superseded_by IS NULL${bucketFilter}`
296
- )
297
- .all(...hydrateParams) as any[];
298
-
299
- const byRowid = new Map<number, any>();
300
- for (const row of hydrated) byRowid.set(row.rowid, row);
301
-
302
- for (const vr of vecRows) {
303
- if (hints.length >= limit) break;
304
- const row = byRowid.get(vr.rowid);
305
- if (!row) continue;
306
-
307
- const similarity = Math.max(0, 1 - vr.distance / 2);
308
- if (similarity < SEMANTIC_SIMILARITY_THRESHOLD) continue;
309
-
310
- // Session dedup
311
- if (sessionSet && !bypassDedup && sessionSet.has(row.id)) {
312
- suppressed++;
313
- continue;
314
- }
315
-
316
- const entryTags: string[] = row.tags ? JSON.parse(row.tags) : [];
317
- hints.push({
318
- id: row.id,
319
- title: row.title || '(untitled)',
320
- summary: row.summary || '',
321
- relevance: similarity >= 0.8 ? 'high' : 'medium',
322
- kind: row.kind || 'knowledge',
323
- tags: entryTags,
324
- });
325
-
326
- if (sessionSet) sessionSet.add(row.id);
327
- }
328
-
329
- if (hints.length > 0) method = 'semantic';
330
- }
331
- }
332
- }
333
- } catch {
334
- // Semantic fallback is best-effort; fast path already ran
335
- }
336
- }
277
+ const method: 'hybrid' | 'none' = hints.length > 0 ? 'hybrid' : 'none';
337
278
 
338
279
  const latency = Date.now() - start;
339
280