context-vault 3.4.5 → 3.5.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.
- package/bin/cli.js +299 -194
- package/dist/server.js +2 -0
- package/dist/server.js.map +1 -1
- package/dist/status.d.ts.map +1 -1
- package/dist/status.js +29 -0
- package/dist/status.js.map +1 -1
- package/dist/tools/context-status.d.ts.map +1 -1
- package/dist/tools/context-status.js +39 -5
- package/dist/tools/context-status.js.map +1 -1
- package/dist/tools/get-context.d.ts.map +1 -1
- package/dist/tools/get-context.js +1 -0
- package/dist/tools/get-context.js.map +1 -1
- package/dist/tools/list-context.d.ts +2 -1
- package/dist/tools/list-context.d.ts.map +1 -1
- package/dist/tools/list-context.js +22 -5
- package/dist/tools/list-context.js.map +1 -1
- package/dist/tools/save-context.d.ts +2 -1
- package/dist/tools/save-context.d.ts.map +1 -1
- package/dist/tools/save-context.js +58 -4
- package/dist/tools/save-context.js.map +1 -1
- package/dist/tools/session-start.d.ts.map +1 -1
- package/dist/tools/session-start.js +192 -7
- package/dist/tools/session-start.js.map +1 -1
- package/node_modules/@context-vault/core/dist/capture.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/capture.js +2 -0
- package/node_modules/@context-vault/core/dist/capture.js.map +1 -1
- package/node_modules/@context-vault/core/dist/config.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/config.js +27 -1
- package/node_modules/@context-vault/core/dist/config.js.map +1 -1
- package/node_modules/@context-vault/core/dist/constants.d.ts +13 -0
- package/node_modules/@context-vault/core/dist/constants.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/constants.js +13 -0
- package/node_modules/@context-vault/core/dist/constants.js.map +1 -1
- package/node_modules/@context-vault/core/dist/db.d.ts +1 -1
- package/node_modules/@context-vault/core/dist/db.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/db.js +73 -9
- package/node_modules/@context-vault/core/dist/db.js.map +1 -1
- package/node_modules/@context-vault/core/dist/index.d.ts +4 -1
- package/node_modules/@context-vault/core/dist/index.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/index.js +58 -10
- package/node_modules/@context-vault/core/dist/index.js.map +1 -1
- package/node_modules/@context-vault/core/dist/indexing.d.ts +8 -0
- package/node_modules/@context-vault/core/dist/indexing.d.ts.map +1 -0
- package/node_modules/@context-vault/core/dist/indexing.js +22 -0
- package/node_modules/@context-vault/core/dist/indexing.js.map +1 -0
- package/node_modules/@context-vault/core/dist/main.d.ts +3 -2
- package/node_modules/@context-vault/core/dist/main.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/main.js +3 -1
- package/node_modules/@context-vault/core/dist/main.js.map +1 -1
- package/node_modules/@context-vault/core/dist/search.d.ts +2 -0
- package/node_modules/@context-vault/core/dist/search.d.ts.map +1 -1
- package/node_modules/@context-vault/core/dist/search.js +82 -6
- package/node_modules/@context-vault/core/dist/search.js.map +1 -1
- package/node_modules/@context-vault/core/dist/types.d.ts +24 -0
- package/node_modules/@context-vault/core/dist/types.d.ts.map +1 -1
- package/node_modules/@context-vault/core/package.json +5 -1
- package/node_modules/@context-vault/core/src/capture.ts +2 -0
- package/node_modules/@context-vault/core/src/config.ts +18 -1
- package/node_modules/@context-vault/core/src/constants.ts +15 -0
- package/node_modules/@context-vault/core/src/db.ts +73 -9
- package/node_modules/@context-vault/core/src/index.ts +65 -11
- package/node_modules/@context-vault/core/src/indexing.ts +35 -0
- package/node_modules/@context-vault/core/src/main.ts +5 -0
- package/node_modules/@context-vault/core/src/search.ts +96 -6
- package/node_modules/@context-vault/core/src/types.ts +26 -0
- package/package.json +2 -2
- package/src/server.ts +3 -0
- package/src/status.ts +35 -0
- package/src/tools/context-status.ts +40 -5
- package/src/tools/get-context.ts +1 -0
- package/src/tools/list-context.ts +20 -5
- package/src/tools/save-context.ts +67 -4
- package/src/tools/session-start.ts +222 -9
|
@@ -143,7 +143,11 @@ export const SCHEMA_DDL = `
|
|
|
143
143
|
last_accessed_at TEXT,
|
|
144
144
|
source_files TEXT,
|
|
145
145
|
tier TEXT DEFAULT 'working' CHECK(tier IN ('ephemeral', 'working', 'durable')),
|
|
146
|
-
related_to TEXT
|
|
146
|
+
related_to TEXT,
|
|
147
|
+
indexed INTEGER DEFAULT 1,
|
|
148
|
+
recall_count INTEGER DEFAULT 0,
|
|
149
|
+
recall_sessions INTEGER DEFAULT 0,
|
|
150
|
+
last_recalled_at TEXT
|
|
147
151
|
);
|
|
148
152
|
|
|
149
153
|
CREATE INDEX IF NOT EXISTS idx_vault_kind ON vault(kind);
|
|
@@ -153,33 +157,42 @@ export const SCHEMA_DDL = `
|
|
|
153
157
|
CREATE UNIQUE INDEX IF NOT EXISTS idx_vault_identity ON vault(kind, identity_key) WHERE identity_key IS NOT NULL AND category = 'entity';
|
|
154
158
|
CREATE INDEX IF NOT EXISTS idx_vault_superseded ON vault(superseded_by) WHERE superseded_by IS NOT NULL;
|
|
155
159
|
CREATE INDEX IF NOT EXISTS idx_vault_tier ON vault(tier);
|
|
160
|
+
CREATE INDEX IF NOT EXISTS idx_vault_indexed ON vault(indexed);
|
|
156
161
|
|
|
157
162
|
CREATE VIRTUAL TABLE IF NOT EXISTS vault_fts USING fts5(
|
|
158
163
|
title, body, tags, kind,
|
|
159
164
|
content='vault', content_rowid='rowid'
|
|
160
165
|
);
|
|
161
166
|
|
|
162
|
-
CREATE TRIGGER IF NOT EXISTS vault_ai AFTER INSERT ON vault BEGIN
|
|
167
|
+
CREATE TRIGGER IF NOT EXISTS vault_ai AFTER INSERT ON vault WHEN new.indexed = 1 BEGIN
|
|
163
168
|
INSERT INTO vault_fts(rowid, title, body, tags, kind)
|
|
164
169
|
VALUES (new.rowid, new.title, new.body, new.tags, new.kind);
|
|
165
170
|
END;
|
|
166
|
-
CREATE TRIGGER IF NOT EXISTS vault_ad AFTER DELETE ON vault BEGIN
|
|
171
|
+
CREATE TRIGGER IF NOT EXISTS vault_ad AFTER DELETE ON vault WHEN old.indexed = 1 BEGIN
|
|
167
172
|
INSERT INTO vault_fts(vault_fts, rowid, title, body, tags, kind)
|
|
168
173
|
VALUES ('delete', old.rowid, old.title, old.body, old.tags, old.kind);
|
|
169
174
|
END;
|
|
170
|
-
CREATE TRIGGER IF NOT EXISTS vault_au AFTER UPDATE ON vault BEGIN
|
|
175
|
+
CREATE TRIGGER IF NOT EXISTS vault_au AFTER UPDATE ON vault WHEN old.indexed = 1 OR new.indexed = 1 BEGIN
|
|
171
176
|
INSERT INTO vault_fts(vault_fts, rowid, title, body, tags, kind)
|
|
172
177
|
VALUES ('delete', old.rowid, old.title, old.body, old.tags, old.kind);
|
|
173
178
|
INSERT INTO vault_fts(rowid, title, body, tags, kind)
|
|
174
|
-
|
|
179
|
+
SELECT new.rowid, new.title, new.body, new.tags, new.kind WHERE new.indexed = 1;
|
|
175
180
|
END;
|
|
176
181
|
|
|
177
182
|
CREATE VIRTUAL TABLE IF NOT EXISTS vault_vec USING vec0(embedding float[384]);
|
|
178
183
|
|
|
179
184
|
CREATE VIRTUAL TABLE IF NOT EXISTS vault_ctx_vec USING vec0(embedding float[384]);
|
|
185
|
+
|
|
186
|
+
CREATE TABLE IF NOT EXISTS co_retrievals (
|
|
187
|
+
entry_a TEXT NOT NULL,
|
|
188
|
+
entry_b TEXT NOT NULL,
|
|
189
|
+
count INTEGER DEFAULT 1,
|
|
190
|
+
last_at TEXT NOT NULL,
|
|
191
|
+
PRIMARY KEY (entry_a, entry_b)
|
|
192
|
+
);
|
|
180
193
|
`;
|
|
181
194
|
|
|
182
|
-
const CURRENT_VERSION =
|
|
195
|
+
const CURRENT_VERSION = 18;
|
|
183
196
|
|
|
184
197
|
export async function initDatabase(dbPath: string): Promise<DatabaseSync> {
|
|
185
198
|
const sqliteVec = await loadSqliteVec();
|
|
@@ -208,9 +221,60 @@ export async function initDatabase(dbPath: string): Promise<DatabaseSync> {
|
|
|
208
221
|
if (version === 15) {
|
|
209
222
|
try {
|
|
210
223
|
db.exec('CREATE VIRTUAL TABLE IF NOT EXISTS vault_ctx_vec USING vec0(embedding float[384])');
|
|
211
|
-
db.exec(
|
|
224
|
+
db.exec('PRAGMA user_version = 16');
|
|
212
225
|
} catch (e) {
|
|
213
226
|
console.error(`[context-vault] v15->v16 migration failed: ${(e as Error).message}`);
|
|
227
|
+
return db;
|
|
228
|
+
}
|
|
229
|
+
// Fall through to v16->v17 migration
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// v16 -> v17: add indexed column for selective indexing
|
|
233
|
+
if (version === 16 || version === 15) {
|
|
234
|
+
try {
|
|
235
|
+
db.exec('ALTER TABLE vault ADD COLUMN indexed INTEGER DEFAULT 1');
|
|
236
|
+
db.exec('CREATE INDEX IF NOT EXISTS idx_vault_indexed ON vault(indexed)');
|
|
237
|
+
db.exec('DROP TRIGGER IF EXISTS vault_ai');
|
|
238
|
+
db.exec('DROP TRIGGER IF EXISTS vault_ad');
|
|
239
|
+
db.exec('DROP TRIGGER IF EXISTS vault_au');
|
|
240
|
+
db.exec(`CREATE TRIGGER vault_ai AFTER INSERT ON vault WHEN new.indexed = 1 BEGIN
|
|
241
|
+
INSERT INTO vault_fts(rowid, title, body, tags, kind)
|
|
242
|
+
VALUES (new.rowid, new.title, new.body, new.tags, new.kind);
|
|
243
|
+
END`);
|
|
244
|
+
db.exec(`CREATE TRIGGER vault_ad AFTER DELETE ON vault WHEN old.indexed = 1 BEGIN
|
|
245
|
+
INSERT INTO vault_fts(vault_fts, rowid, title, body, tags, kind)
|
|
246
|
+
VALUES ('delete', old.rowid, old.title, old.body, old.tags, old.kind);
|
|
247
|
+
END`);
|
|
248
|
+
db.exec(`CREATE TRIGGER vault_au AFTER UPDATE ON vault WHEN old.indexed = 1 OR new.indexed = 1 BEGIN
|
|
249
|
+
INSERT INTO vault_fts(vault_fts, rowid, title, body, tags, kind)
|
|
250
|
+
VALUES ('delete', old.rowid, old.title, old.body, old.tags, old.kind);
|
|
251
|
+
INSERT INTO vault_fts(rowid, title, body, tags, kind)
|
|
252
|
+
SELECT new.rowid, new.title, new.body, new.tags, new.kind WHERE new.indexed = 1;
|
|
253
|
+
END`);
|
|
254
|
+
db.exec('PRAGMA user_version = 17');
|
|
255
|
+
} catch (e) {
|
|
256
|
+
console.error(`[context-vault] v16->v17 migration failed: ${(e as Error).message}`);
|
|
257
|
+
return db;
|
|
258
|
+
}
|
|
259
|
+
// Fall through to v17->v18 migration
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// v17 -> v18: add recall frequency tracking columns and co_retrievals table
|
|
263
|
+
if (version === 17 || version === 16 || version === 15) {
|
|
264
|
+
try {
|
|
265
|
+
db.exec('ALTER TABLE vault ADD COLUMN recall_count INTEGER DEFAULT 0');
|
|
266
|
+
db.exec('ALTER TABLE vault ADD COLUMN recall_sessions INTEGER DEFAULT 0');
|
|
267
|
+
db.exec('ALTER TABLE vault ADD COLUMN last_recalled_at TEXT');
|
|
268
|
+
db.exec(`CREATE TABLE IF NOT EXISTS co_retrievals (
|
|
269
|
+
entry_a TEXT NOT NULL,
|
|
270
|
+
entry_b TEXT NOT NULL,
|
|
271
|
+
count INTEGER DEFAULT 1,
|
|
272
|
+
last_at TEXT NOT NULL,
|
|
273
|
+
PRIMARY KEY (entry_a, entry_b)
|
|
274
|
+
)`);
|
|
275
|
+
db.exec(`PRAGMA user_version = ${CURRENT_VERSION}`);
|
|
276
|
+
} catch (e) {
|
|
277
|
+
console.error(`[context-vault] v17->v18 migration failed: ${(e as Error).message}`);
|
|
214
278
|
}
|
|
215
279
|
return db;
|
|
216
280
|
}
|
|
@@ -256,7 +320,7 @@ export async function initDatabase(dbPath: string): Promise<DatabaseSync> {
|
|
|
256
320
|
return freshDb;
|
|
257
321
|
}
|
|
258
322
|
|
|
259
|
-
if (version <
|
|
323
|
+
if (version < 18) {
|
|
260
324
|
db.exec(SCHEMA_DDL);
|
|
261
325
|
db.exec(`PRAGMA user_version = ${CURRENT_VERSION}`);
|
|
262
326
|
}
|
|
@@ -271,7 +335,7 @@ export function prepareStatements(db: DatabaseSync): PreparedStatements {
|
|
|
271
335
|
try {
|
|
272
336
|
return {
|
|
273
337
|
insertEntry: db.prepare(
|
|
274
|
-
`INSERT INTO vault (id, kind, category, title, body, meta, tags, source, file_path, identity_key, expires_at, created_at, updated_at, source_files, tier) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
338
|
+
`INSERT INTO vault (id, kind, category, title, body, meta, tags, source, file_path, identity_key, expires_at, created_at, updated_at, source_files, tier, indexed) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`
|
|
275
339
|
),
|
|
276
340
|
updateEntry: db.prepare(
|
|
277
341
|
`UPDATE vault SET title = ?, body = ?, meta = ?, tags = ?, source = ?, category = ?, identity_key = ?, expires_at = ?, updated_at = datetime('now') WHERE file_path = ?`
|
|
@@ -4,7 +4,9 @@ import { dirToKind, walkDir, ulid } from './files.js';
|
|
|
4
4
|
import { categoryFor, defaultTierFor, CATEGORY_DIRS } from './categories.js';
|
|
5
5
|
import { parseFrontmatter, parseEntryFromMarkdown } from './frontmatter.js';
|
|
6
6
|
import { embedBatch } from './embed.js';
|
|
7
|
-
import type { BaseCtx, IndexEntryInput, ReindexStats } from './types.js';
|
|
7
|
+
import type { BaseCtx, IndexEntryInput, IndexingConfig, ReindexStats } from './types.js';
|
|
8
|
+
import { shouldIndex } from './indexing.js';
|
|
9
|
+
import { DEFAULT_INDEXING } from './constants.js';
|
|
8
10
|
|
|
9
11
|
const EXCLUDED_DIRS = new Set(['projects', '_archive']);
|
|
10
12
|
const EXCLUDED_FILES = new Set(['context.md', 'memory.md', 'README.md']);
|
|
@@ -33,6 +35,7 @@ export async function indexEntry(
|
|
|
33
35
|
expires_at,
|
|
34
36
|
source_files,
|
|
35
37
|
tier,
|
|
38
|
+
indexed = true,
|
|
36
39
|
} = entry;
|
|
37
40
|
|
|
38
41
|
if (expires_at && new Date(expires_at) <= new Date()) return;
|
|
@@ -84,7 +87,8 @@ export async function indexEntry(
|
|
|
84
87
|
createdAt,
|
|
85
88
|
createdAt,
|
|
86
89
|
sourceFilesJson,
|
|
87
|
-
effectiveTier
|
|
90
|
+
effectiveTier,
|
|
91
|
+
indexed ? 1 : 0
|
|
88
92
|
);
|
|
89
93
|
} catch (e) {
|
|
90
94
|
if ((e as Error).message.includes('UNIQUE constraint')) {
|
|
@@ -132,7 +136,7 @@ export async function indexEntry(
|
|
|
132
136
|
);
|
|
133
137
|
}
|
|
134
138
|
|
|
135
|
-
if (cat !== 'event') {
|
|
139
|
+
if (indexed && cat !== 'event') {
|
|
136
140
|
let embedding: Float32Array | null = null;
|
|
137
141
|
if (precomputedEmbedding !== undefined) {
|
|
138
142
|
embedding = precomputedEmbedding;
|
|
@@ -184,14 +188,17 @@ export async function pruneExpired(ctx: BaseCtx): Promise<number> {
|
|
|
184
188
|
|
|
185
189
|
export async function reindex(
|
|
186
190
|
ctx: BaseCtx,
|
|
187
|
-
opts: { fullSync?: boolean } = {}
|
|
191
|
+
opts: { fullSync?: boolean; indexingConfig?: IndexingConfig; dryRun?: boolean; kindFilter?: string } = {}
|
|
188
192
|
): Promise<ReindexStats> {
|
|
189
|
-
const { fullSync = true } = opts;
|
|
193
|
+
const { fullSync = true, indexingConfig, dryRun = false, kindFilter: reindexKindFilter } = opts;
|
|
194
|
+
const ixConfig = indexingConfig ?? (ctx.config as any)?.indexing ?? DEFAULT_INDEXING;
|
|
190
195
|
const stats: ReindexStats = {
|
|
191
196
|
added: 0,
|
|
192
197
|
updated: 0,
|
|
193
198
|
removed: 0,
|
|
194
199
|
unchanged: 0,
|
|
200
|
+
skippedIndexing: 0,
|
|
201
|
+
embeddingsCleared: 0,
|
|
195
202
|
};
|
|
196
203
|
|
|
197
204
|
if (!existsSync(ctx.config.vaultDir)) return stats;
|
|
@@ -225,17 +232,44 @@ export async function reindex(
|
|
|
225
232
|
}
|
|
226
233
|
}
|
|
227
234
|
|
|
235
|
+
const filteredKindEntries = reindexKindFilter
|
|
236
|
+
? kindEntries.filter((ke) => ke.kind === reindexKindFilter)
|
|
237
|
+
: kindEntries;
|
|
238
|
+
|
|
228
239
|
const pendingEmbeds: { rowid: number; text: string }[] = [];
|
|
229
240
|
|
|
241
|
+
if (dryRun) {
|
|
242
|
+
for (const { kind, dir } of filteredKindEntries) {
|
|
243
|
+
const category = categoryFor(kind);
|
|
244
|
+
const mdFiles = walkDir(dir).filter((f) => !EXCLUDED_FILES.has(basename(f.filePath)));
|
|
245
|
+
for (const { filePath } of mdFiles) {
|
|
246
|
+
const raw = readFileSync(filePath, 'utf-8');
|
|
247
|
+
if (!raw.startsWith('---\n')) continue;
|
|
248
|
+
const { meta: fmMeta, body: rawBody } = parseFrontmatter(raw);
|
|
249
|
+
const parsed = parseEntryFromMarkdown(kind, rawBody, fmMeta);
|
|
250
|
+
const entryIndexed = shouldIndex(
|
|
251
|
+
{ kind, category, bodyLength: parsed.body.length },
|
|
252
|
+
ixConfig
|
|
253
|
+
);
|
|
254
|
+
if (entryIndexed) {
|
|
255
|
+
stats.added++;
|
|
256
|
+
} else {
|
|
257
|
+
stats.skippedIndexing!++;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return stats;
|
|
262
|
+
}
|
|
263
|
+
|
|
230
264
|
ctx.db.exec('BEGIN');
|
|
231
265
|
try {
|
|
232
|
-
for (const { kind, dir } of
|
|
266
|
+
for (const { kind, dir } of filteredKindEntries) {
|
|
233
267
|
const category = categoryFor(kind);
|
|
234
268
|
const mdFiles = walkDir(dir).filter((f) => !EXCLUDED_FILES.has(basename(f.filePath)));
|
|
235
269
|
|
|
236
270
|
const dbRows = ctx.db
|
|
237
271
|
.prepare(
|
|
238
|
-
'SELECT id, file_path, body, title, tags, meta, related_to FROM vault WHERE kind = ?'
|
|
272
|
+
'SELECT id, file_path, body, title, tags, meta, related_to, indexed FROM vault WHERE kind = ?'
|
|
239
273
|
)
|
|
240
274
|
.all(kind) as Record<string, unknown>[];
|
|
241
275
|
const dbByPath = new Map(dbRows.map((r) => [r.file_path as string, r]));
|
|
@@ -269,6 +303,11 @@ export async function reindex(
|
|
|
269
303
|
else delete meta.folder;
|
|
270
304
|
const metaJson = Object.keys(meta).length ? JSON.stringify(meta) : null;
|
|
271
305
|
|
|
306
|
+
const entryIndexed = shouldIndex(
|
|
307
|
+
{ kind, category, bodyLength: parsed.body.length },
|
|
308
|
+
ixConfig
|
|
309
|
+
);
|
|
310
|
+
|
|
272
311
|
if (!existing) {
|
|
273
312
|
const id = (fmMeta.id as string) || ulid();
|
|
274
313
|
const tagsJson = fmMeta.tags ? JSON.stringify(fmMeta.tags) : null;
|
|
@@ -290,10 +329,11 @@ export async function reindex(
|
|
|
290
329
|
(fmMeta.updated as string) || created
|
|
291
330
|
);
|
|
292
331
|
if ((result as { changes: number }).changes > 0) {
|
|
332
|
+
ctx.db.prepare('UPDATE vault SET indexed = ? WHERE id = ?').run(entryIndexed ? 1 : 0, id);
|
|
293
333
|
if (relatedToJson && ctx.stmts.updateRelatedTo) {
|
|
294
334
|
ctx.stmts.updateRelatedTo.run(relatedToJson, id);
|
|
295
335
|
}
|
|
296
|
-
if (category !== 'event') {
|
|
336
|
+
if (entryIndexed && category !== 'event') {
|
|
297
337
|
const rowidResult = ctx.stmts.getRowid.get(id) as { rowid: number } | undefined;
|
|
298
338
|
if (rowidResult?.rowid) {
|
|
299
339
|
const embeddingText = [parsed.title, parsed.body].filter(Boolean).join(' ');
|
|
@@ -303,6 +343,7 @@ export async function reindex(
|
|
|
303
343
|
});
|
|
304
344
|
}
|
|
305
345
|
}
|
|
346
|
+
if (!entryIndexed) stats.skippedIndexing!++;
|
|
306
347
|
stats.added++;
|
|
307
348
|
} else {
|
|
308
349
|
stats.unchanged++;
|
|
@@ -315,7 +356,10 @@ export async function reindex(
|
|
|
315
356
|
const metaChanged = metaJson !== ((existing.meta as string) || null);
|
|
316
357
|
const relatedToChanged = relatedToJson !== ((existing.related_to as string) || null);
|
|
317
358
|
|
|
318
|
-
|
|
359
|
+
const existingIndexed = (existing as any).indexed;
|
|
360
|
+
const indexedChanged = (entryIndexed ? 1 : 0) !== (existingIndexed ?? 1);
|
|
361
|
+
|
|
362
|
+
if (bodyChanged || titleChanged || tagsChanged || metaChanged || relatedToChanged || indexedChanged) {
|
|
319
363
|
ctx.stmts.updateEntry.run(
|
|
320
364
|
parsed.title || null,
|
|
321
365
|
parsed.body,
|
|
@@ -327,11 +371,20 @@ export async function reindex(
|
|
|
327
371
|
expires_at,
|
|
328
372
|
filePath
|
|
329
373
|
);
|
|
330
|
-
|
|
374
|
+
ctx.db.prepare('UPDATE vault SET indexed = ? WHERE file_path = ?').run(entryIndexed ? 1 : 0, filePath);
|
|
375
|
+
if (relatedToJson && ctx.stmts.updateRelatedTo) {
|
|
331
376
|
ctx.stmts.updateRelatedTo.run(relatedToJson, existing.id as string);
|
|
332
377
|
}
|
|
333
378
|
|
|
334
|
-
if (
|
|
379
|
+
if (!entryIndexed) {
|
|
380
|
+
const rowid = (
|
|
381
|
+
ctx.stmts.getRowid.get(existing.id as string) as { rowid: number } | undefined
|
|
382
|
+
)?.rowid;
|
|
383
|
+
if (rowid) {
|
|
384
|
+
try { ctx.deleteVec(rowid); stats.embeddingsCleared!++; } catch {}
|
|
385
|
+
}
|
|
386
|
+
stats.skippedIndexing!++;
|
|
387
|
+
} else if ((bodyChanged || titleChanged) && category !== 'event') {
|
|
335
388
|
const rowid = (
|
|
336
389
|
ctx.stmts.getRowid.get(existing.id as string) as { rowid: number } | undefined
|
|
337
390
|
)?.rowid;
|
|
@@ -435,6 +488,7 @@ export async function reindex(
|
|
|
435
488
|
.prepare(
|
|
436
489
|
`SELECT v.rowid, v.title, v.body FROM vault v
|
|
437
490
|
WHERE v.category != 'event'
|
|
491
|
+
AND v.indexed = 1
|
|
438
492
|
AND v.rowid NOT IN (SELECT rowid FROM vault_vec)`
|
|
439
493
|
)
|
|
440
494
|
.all() as { rowid: number; title: string | null; body: string }[];
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { IndexingConfig } from './types.js';
|
|
2
|
+
import { DEFAULT_INDEXING } from './constants.js';
|
|
3
|
+
|
|
4
|
+
export function shouldIndex(
|
|
5
|
+
opts: {
|
|
6
|
+
kind: string;
|
|
7
|
+
category: string;
|
|
8
|
+
bodyLength: number;
|
|
9
|
+
explicitIndexed?: boolean;
|
|
10
|
+
},
|
|
11
|
+
config?: IndexingConfig | null
|
|
12
|
+
): boolean {
|
|
13
|
+
if (opts.explicitIndexed === true) return true;
|
|
14
|
+
if (opts.explicitIndexed === false) return false;
|
|
15
|
+
|
|
16
|
+
const c = config ?? DEFAULT_INDEXING;
|
|
17
|
+
|
|
18
|
+
if (c.excludeKinds.length > 0 && c.excludeKinds.includes(opts.kind)) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (c.excludeCategories.length > 0 && c.excludeCategories.includes(opts.category)) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
if (opts.category === 'event' && !c.autoIndexEvents) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (c.maxBodySize > 0 && opts.bodyLength > c.maxBodySize) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
@@ -4,6 +4,7 @@ export type {
|
|
|
4
4
|
RecallConfig,
|
|
5
5
|
ConsolidationConfig,
|
|
6
6
|
GrowthThresholds,
|
|
7
|
+
IndexingConfig,
|
|
7
8
|
PreparedStatements,
|
|
8
9
|
VaultEntry,
|
|
9
10
|
SearchResult,
|
|
@@ -31,6 +32,7 @@ export {
|
|
|
31
32
|
MAX_IDENTITY_KEY_LENGTH,
|
|
32
33
|
DEFAULT_GROWTH_THRESHOLDS,
|
|
33
34
|
DEFAULT_LIFECYCLE,
|
|
35
|
+
DEFAULT_INDEXING,
|
|
34
36
|
} from './constants.js';
|
|
35
37
|
|
|
36
38
|
// Categories
|
|
@@ -99,5 +101,8 @@ export {
|
|
|
99
101
|
// Capture
|
|
100
102
|
export { writeEntry, updateEntryFile, captureAndIndex } from './capture.js';
|
|
101
103
|
|
|
104
|
+
// Indexing
|
|
105
|
+
export { shouldIndex } from './indexing.js';
|
|
106
|
+
|
|
102
107
|
// Ingest URL
|
|
103
108
|
export { htmlToMarkdown, extractHtmlContent, ingestUrl } from './ingest-url.js';
|
|
@@ -2,6 +2,9 @@ import type { BaseCtx, SearchResult, SearchOptions, VaultEntry } from './types.j
|
|
|
2
2
|
|
|
3
3
|
const NEAR_DUP_THRESHOLD = 0.92;
|
|
4
4
|
const RRF_K = 60;
|
|
5
|
+
const RECALL_BOOST_CAP = 2.0;
|
|
6
|
+
const RECALL_HALF_LIFE_DAYS = 30;
|
|
7
|
+
const DISCOVERY_SLOTS = 2;
|
|
5
8
|
|
|
6
9
|
export function recencyDecayScore(updatedAt: string | null | undefined, decayRate = 0.05): number {
|
|
7
10
|
if (updatedAt == null) return 0.5;
|
|
@@ -34,6 +37,18 @@ export function recencyBoost(createdAt: string, category: string, decayDays = 30
|
|
|
34
37
|
return 1 / (1 + ageDays / decayDays);
|
|
35
38
|
}
|
|
36
39
|
|
|
40
|
+
export function recallBoost(recallCount: number, lastRecalledAt: string | null): number {
|
|
41
|
+
if (recallCount <= 0) return 1.0;
|
|
42
|
+
const logBoost = Math.log(recallCount + 1);
|
|
43
|
+
let recencyWeight = 1.0;
|
|
44
|
+
if (lastRecalledAt) {
|
|
45
|
+
const ageDays = (Date.now() - new Date(lastRecalledAt).getTime()) / 86400000;
|
|
46
|
+
recencyWeight = Math.pow(0.5, ageDays / RECALL_HALF_LIFE_DAYS);
|
|
47
|
+
}
|
|
48
|
+
const boost = 1 + logBoost * recencyWeight;
|
|
49
|
+
return Math.min(boost, RECALL_BOOST_CAP);
|
|
50
|
+
}
|
|
51
|
+
|
|
37
52
|
export function buildFilterClauses({
|
|
38
53
|
categoryFilter,
|
|
39
54
|
excludeEvents = false,
|
|
@@ -73,6 +88,7 @@ export function buildFilterClauses({
|
|
|
73
88
|
if (!includeEphemeral) {
|
|
74
89
|
clauses.push("e.tier != 'ephemeral'");
|
|
75
90
|
}
|
|
91
|
+
clauses.push('e.indexed = 1');
|
|
76
92
|
return { clauses, params };
|
|
77
93
|
}
|
|
78
94
|
|
|
@@ -191,6 +207,7 @@ export async function hybridSearch(
|
|
|
191
207
|
if (since && row.created_at < since) continue;
|
|
192
208
|
if (until && row.created_at > until) continue;
|
|
193
209
|
if (row.expires_at && new Date(row.expires_at) <= new Date()) continue;
|
|
210
|
+
if (!row.indexed) continue;
|
|
194
211
|
|
|
195
212
|
const { rowid: _rowid, ...cleanRow } = row;
|
|
196
213
|
idToRowid.set(cleanRow.id, Number(row.rowid));
|
|
@@ -243,6 +260,7 @@ export async function hybridSearch(
|
|
|
243
260
|
if (since && row.created_at < since) continue;
|
|
244
261
|
if (until && row.created_at > until) continue;
|
|
245
262
|
if (row.expires_at && new Date(row.expires_at) <= new Date()) continue;
|
|
263
|
+
if (!row.indexed) continue;
|
|
246
264
|
|
|
247
265
|
const { rowid: _rowid, ...cleanRow } = row;
|
|
248
266
|
ctxRankedIds.push(cleanRow.id);
|
|
@@ -267,7 +285,11 @@ export async function hybridSearch(
|
|
|
267
285
|
|
|
268
286
|
for (const [id, entry] of rowMap) {
|
|
269
287
|
const boost = recencyBoost(entry.created_at, entry.category, decayDays);
|
|
270
|
-
|
|
288
|
+
const recall = recallBoost(
|
|
289
|
+
entry.recall_count ?? 0,
|
|
290
|
+
entry.last_recalled_at ?? null
|
|
291
|
+
);
|
|
292
|
+
rrfScores.set(id, (rrfScores.get(id) ?? 0) * boost * recall);
|
|
271
293
|
}
|
|
272
294
|
|
|
273
295
|
const candidates: SearchResult[] = [...rowMap.values()].map((entry) => ({
|
|
@@ -316,26 +338,94 @@ export async function hybridSearch(
|
|
|
316
338
|
selected.push(candidate);
|
|
317
339
|
if (vec) selectedVecs.push(vec);
|
|
318
340
|
}
|
|
319
|
-
const dedupedPage = selected.slice(offset, offset + limit);
|
|
341
|
+
const dedupedPage = injectDiscoverySlots(selected.slice(offset, offset + limit), candidates);
|
|
320
342
|
trackAccess(ctx, dedupedPage);
|
|
321
343
|
return dedupedPage;
|
|
322
344
|
}
|
|
323
345
|
|
|
324
|
-
const
|
|
346
|
+
const page = candidates.slice(offset, offset + limit);
|
|
347
|
+
const finalPage = injectDiscoverySlots(page, candidates);
|
|
325
348
|
trackAccess(ctx, finalPage);
|
|
326
349
|
return finalPage;
|
|
327
350
|
}
|
|
328
351
|
|
|
352
|
+
function injectDiscoverySlots(
|
|
353
|
+
page: SearchResult[],
|
|
354
|
+
allCandidates: SearchResult[]
|
|
355
|
+
): SearchResult[] {
|
|
356
|
+
if (page.length < 4) return page;
|
|
357
|
+
const pageIds = new Set(page.map((e) => e.id));
|
|
358
|
+
const discoveries = allCandidates
|
|
359
|
+
.filter(
|
|
360
|
+
(c) =>
|
|
361
|
+
!pageIds.has(c.id) &&
|
|
362
|
+
(c.recall_count ?? 0) <= 2 &&
|
|
363
|
+
c.score > 0
|
|
364
|
+
)
|
|
365
|
+
.slice(0, DISCOVERY_SLOTS);
|
|
366
|
+
if (!discoveries.length) return page;
|
|
367
|
+
const result = [...page];
|
|
368
|
+
for (let i = 0; i < discoveries.length && result.length > 2; i++) {
|
|
369
|
+
result.splice(result.length - 1 - i, 1, discoveries[i]);
|
|
370
|
+
}
|
|
371
|
+
return result;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
let _sessionId: string | null = null;
|
|
375
|
+
const _seenSessionIds = new Set<string>();
|
|
376
|
+
|
|
377
|
+
export function setSessionId(id: string): void {
|
|
378
|
+
_sessionId = id;
|
|
379
|
+
}
|
|
380
|
+
|
|
329
381
|
export function trackAccess(ctx: BaseCtx, entries: SearchResult[]): void {
|
|
330
382
|
if (!entries.length) return;
|
|
383
|
+
|
|
384
|
+
const ids = entries.map((e) => e.id);
|
|
385
|
+
const now = new Date().toISOString();
|
|
386
|
+
|
|
331
387
|
try {
|
|
332
|
-
const placeholders =
|
|
388
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
333
389
|
ctx.db
|
|
334
390
|
.prepare(
|
|
335
|
-
`UPDATE vault SET hit_count = hit_count + 1, last_accessed_at = datetime('now') WHERE id IN (${placeholders})`
|
|
391
|
+
`UPDATE vault SET hit_count = hit_count + 1, last_accessed_at = datetime('now'), recall_count = recall_count + 1, last_recalled_at = ? WHERE id IN (${placeholders})`
|
|
336
392
|
)
|
|
337
|
-
.run(...
|
|
393
|
+
.run(now, ...ids);
|
|
338
394
|
} catch {
|
|
339
395
|
// Non-fatal
|
|
340
396
|
}
|
|
397
|
+
|
|
398
|
+
const sessionId = _sessionId || 'default';
|
|
399
|
+
const sessionKey = `${sessionId}:${ids.sort().join(',')}`;
|
|
400
|
+
const isNewSession = !_seenSessionIds.has(sessionKey);
|
|
401
|
+
if (isNewSession) {
|
|
402
|
+
_seenSessionIds.add(sessionKey);
|
|
403
|
+
try {
|
|
404
|
+
const placeholders = ids.map(() => '?').join(',');
|
|
405
|
+
ctx.db
|
|
406
|
+
.prepare(
|
|
407
|
+
`UPDATE vault SET recall_sessions = recall_sessions + 1 WHERE id IN (${placeholders})`
|
|
408
|
+
)
|
|
409
|
+
.run(...ids);
|
|
410
|
+
} catch {
|
|
411
|
+
// Non-fatal
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (ids.length >= 2) {
|
|
416
|
+
try {
|
|
417
|
+
const upsert = ctx.db.prepare(
|
|
418
|
+
`INSERT INTO co_retrievals (entry_a, entry_b, count, last_at) VALUES (?, ?, 1, ?)
|
|
419
|
+
ON CONFLICT(entry_a, entry_b) DO UPDATE SET count = count + 1, last_at = ?`
|
|
420
|
+
);
|
|
421
|
+
for (let i = 0; i < ids.length; i++) {
|
|
422
|
+
for (let j = i + 1; j < ids.length; j++) {
|
|
423
|
+
const [a, b] = ids[i] < ids[j] ? [ids[i], ids[j]] : [ids[j], ids[i]];
|
|
424
|
+
upsert.run(a, b, now, now);
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
} catch {
|
|
428
|
+
// Non-fatal
|
|
429
|
+
}
|
|
430
|
+
}
|
|
341
431
|
}
|
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
import type { DatabaseSync, StatementSync } from 'node:sqlite';
|
|
2
2
|
|
|
3
|
+
export interface AutoInsightsConfig {
|
|
4
|
+
enabled: boolean;
|
|
5
|
+
patterns: string[];
|
|
6
|
+
minChars: number;
|
|
7
|
+
maxPerSession: number;
|
|
8
|
+
tier: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface IndexingConfig {
|
|
12
|
+
excludeKinds: string[];
|
|
13
|
+
excludeCategories: string[];
|
|
14
|
+
maxBodySize: number;
|
|
15
|
+
autoIndexEvents: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
3
18
|
export interface VaultConfig {
|
|
4
19
|
vaultDir: string;
|
|
5
20
|
dataDir: string;
|
|
@@ -14,6 +29,8 @@ export interface VaultConfig {
|
|
|
14
29
|
recall: RecallConfig;
|
|
15
30
|
consolidation: ConsolidationConfig;
|
|
16
31
|
lifecycle: Record<string, { archiveAfterDays?: number }>;
|
|
32
|
+
autoInsights: AutoInsightsConfig;
|
|
33
|
+
indexing: IndexingConfig;
|
|
17
34
|
}
|
|
18
35
|
|
|
19
36
|
export interface RecallConfig {
|
|
@@ -77,6 +94,10 @@ export interface VaultEntry {
|
|
|
77
94
|
source_files: string | null;
|
|
78
95
|
tier: string;
|
|
79
96
|
related_to: string | null;
|
|
97
|
+
indexed: number;
|
|
98
|
+
recall_count: number;
|
|
99
|
+
recall_sessions: number;
|
|
100
|
+
last_recalled_at: string | null;
|
|
80
101
|
rowid?: number;
|
|
81
102
|
}
|
|
82
103
|
|
|
@@ -100,6 +121,7 @@ export interface CaptureInput {
|
|
|
100
121
|
related_to?: string[] | null;
|
|
101
122
|
source_files?: Array<{ path: string; hash: string }> | null;
|
|
102
123
|
tier?: string | null;
|
|
124
|
+
indexed?: boolean;
|
|
103
125
|
}
|
|
104
126
|
|
|
105
127
|
export interface CaptureResult {
|
|
@@ -120,6 +142,7 @@ export interface CaptureResult {
|
|
|
120
142
|
related_to: string[] | null;
|
|
121
143
|
source_files: Array<{ path: string; hash: string }> | null;
|
|
122
144
|
tier: string | null;
|
|
145
|
+
indexed: boolean;
|
|
123
146
|
}
|
|
124
147
|
|
|
125
148
|
export interface IndexEntryInput {
|
|
@@ -137,6 +160,7 @@ export interface IndexEntryInput {
|
|
|
137
160
|
expires_at: string | null;
|
|
138
161
|
source_files: Array<{ path: string; hash: string }> | null;
|
|
139
162
|
tier: string | null;
|
|
163
|
+
indexed?: boolean;
|
|
140
164
|
}
|
|
141
165
|
|
|
142
166
|
export interface ReindexStats {
|
|
@@ -144,6 +168,8 @@ export interface ReindexStats {
|
|
|
144
168
|
updated: number;
|
|
145
169
|
removed: number;
|
|
146
170
|
unchanged: number;
|
|
171
|
+
skippedIndexing?: number;
|
|
172
|
+
embeddingsCleared?: number;
|
|
147
173
|
}
|
|
148
174
|
|
|
149
175
|
export interface BaseCtx {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "context-vault",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.5.0",
|
|
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.
|
|
66
|
+
"@context-vault/core": "^3.5.0",
|
|
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
|
@@ -31,6 +31,7 @@ import {
|
|
|
31
31
|
} from '@context-vault/core/db';
|
|
32
32
|
import { registerTools } from './register-tools.js';
|
|
33
33
|
import { pruneExpired } from '@context-vault/core/index';
|
|
34
|
+
import { setSessionId } from '@context-vault/core/search';
|
|
34
35
|
|
|
35
36
|
const DAEMON_PORT = 3377;
|
|
36
37
|
const PID_PATH = join(homedir(), '.context-mcp', 'daemon.pid');
|
|
@@ -359,6 +360,8 @@ async function main(): Promise<void> {
|
|
|
359
360
|
toolStats: { ok: 0, errors: 0, lastError: null },
|
|
360
361
|
};
|
|
361
362
|
|
|
363
|
+
setSessionId(randomUUID());
|
|
364
|
+
|
|
362
365
|
try {
|
|
363
366
|
const pruned = await pruneExpired(ctx);
|
|
364
367
|
if (pruned > 0) {
|