@velvetmonkey/flywheel-memory 2.0.119 → 2.0.121

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 (2) hide show
  1. package/dist/index.js +531 -214
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -128,17 +128,23 @@ var init_levenshtein = __esm({
128
128
  });
129
129
 
130
130
  // src/vault-scope.ts
131
+ import { AsyncLocalStorage } from "node:async_hooks";
131
132
  function getActiveScopeOrNull() {
132
- return activeScope;
133
+ return vaultAls.getStore() ?? fallbackScope;
133
134
  }
134
- function setActiveScope(scope) {
135
- activeScope = scope;
135
+ function runInVaultScope(scope, fn) {
136
+ return vaultAls.run(scope, fn);
136
137
  }
137
- var activeScope;
138
+ function setFallbackScope(scope) {
139
+ fallbackScope = scope;
140
+ }
141
+ var vaultAls, fallbackScope, setActiveScope;
138
142
  var init_vault_scope = __esm({
139
143
  "src/vault-scope.ts"() {
140
144
  "use strict";
141
- activeScope = null;
145
+ vaultAls = new AsyncLocalStorage();
146
+ fallbackScope = null;
147
+ setActiveScope = setFallbackScope;
142
148
  }
143
149
  });
144
150
 
@@ -155,14 +161,22 @@ function getModelConfig() {
155
161
  function getActiveModelId() {
156
162
  return activeModelConfig.id;
157
163
  }
164
+ function getDb() {
165
+ return getActiveScopeOrNull()?.stateDb?.db ?? db;
166
+ }
167
+ function getEmbMap() {
168
+ return getActiveScopeOrNull()?.entityEmbeddingsMap ?? entityEmbeddingsMap;
169
+ }
158
170
  function getEmbeddingsBuildState() {
159
- if (!db) return "none";
160
- const row = db.prepare(`SELECT value FROM fts_metadata WHERE key = 'embeddings_state'`).get();
171
+ const db4 = getDb();
172
+ if (!db4) return "none";
173
+ const row = db4.prepare(`SELECT value FROM fts_metadata WHERE key = 'embeddings_state'`).get();
161
174
  return row?.value || "none";
162
175
  }
163
176
  function setEmbeddingsBuildState(state2) {
164
- if (!db) return;
165
- db.prepare(`INSERT OR REPLACE INTO fts_metadata (key, value) VALUES ('embeddings_state', ?)`).run(state2);
177
+ const db4 = getDb();
178
+ if (!db4) return;
179
+ db4.prepare(`INSERT OR REPLACE INTO fts_metadata (key, value) VALUES ('embeddings_state', ?)`).run(state2);
166
180
  }
167
181
  function setEmbeddingsDatabase(database) {
168
182
  db = database;
@@ -273,7 +287,8 @@ function shouldIndexFile(filePath) {
273
287
  return !parts.some((part) => EXCLUDED_DIRS2.has(part));
274
288
  }
275
289
  async function buildEmbeddingsIndex(vaultPath2, onProgress) {
276
- if (!db) {
290
+ const db4 = getDb();
291
+ if (!db4) {
277
292
  throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
278
293
  }
279
294
  embeddingsBuilding = true;
@@ -282,11 +297,11 @@ async function buildEmbeddingsIndex(vaultPath2, onProgress) {
282
297
  const files = await scanVault(vaultPath2);
283
298
  const indexable = files.filter((f) => shouldIndexFile(f.path));
284
299
  const existingHashes = /* @__PURE__ */ new Map();
285
- const rows = db.prepare("SELECT path, content_hash FROM note_embeddings").all();
300
+ const rows = db4.prepare("SELECT path, content_hash FROM note_embeddings").all();
286
301
  for (const row of rows) {
287
302
  existingHashes.set(row.path, row.content_hash);
288
303
  }
289
- const upsert = db.prepare(`
304
+ const upsert = db4.prepare(`
290
305
  INSERT OR REPLACE INTO note_embeddings (path, embedding, content_hash, model, updated_at)
291
306
  VALUES (?, ?, ?, ?, ?)
292
307
  `);
@@ -318,7 +333,7 @@ async function buildEmbeddingsIndex(vaultPath2, onProgress) {
318
333
  if (onProgress) onProgress(progress);
319
334
  }
320
335
  const currentPaths = new Set(indexable.map((f) => f.path));
321
- const deleteStmt = db.prepare("DELETE FROM note_embeddings WHERE path = ?");
336
+ const deleteStmt = db4.prepare("DELETE FROM note_embeddings WHERE path = ?");
322
337
  for (const existingPath of existingHashes.keys()) {
323
338
  if (!currentPaths.has(existingPath)) {
324
339
  deleteStmt.run(existingPath);
@@ -329,15 +344,16 @@ async function buildEmbeddingsIndex(vaultPath2, onProgress) {
329
344
  return progress;
330
345
  }
331
346
  async function updateEmbedding(notePath, absolutePath) {
332
- if (!db) return;
347
+ const db4 = getDb();
348
+ if (!db4) return;
333
349
  try {
334
350
  const content = fs3.readFileSync(absolutePath, "utf-8");
335
351
  const hash = contentHash(content);
336
- const existing = db.prepare("SELECT content_hash FROM note_embeddings WHERE path = ?").get(notePath);
352
+ const existing = db4.prepare("SELECT content_hash FROM note_embeddings WHERE path = ?").get(notePath);
337
353
  if (existing?.content_hash === hash) return;
338
354
  const embedding = await embedText(content);
339
355
  const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
340
- db.prepare(`
356
+ db4.prepare(`
341
357
  INSERT OR REPLACE INTO note_embeddings (path, embedding, content_hash, model, updated_at)
342
358
  VALUES (?, ?, ?, ?, ?)
343
359
  `).run(notePath, buf, hash, activeModelConfig.id, Date.now());
@@ -345,8 +361,9 @@ async function updateEmbedding(notePath, absolutePath) {
345
361
  }
346
362
  }
347
363
  function removeEmbedding(notePath) {
348
- if (!db) return;
349
- db.prepare("DELETE FROM note_embeddings WHERE path = ?").run(notePath);
364
+ const db4 = getDb();
365
+ if (!db4) return;
366
+ db4.prepare("DELETE FROM note_embeddings WHERE path = ?").run(notePath);
350
367
  }
351
368
  function cosineSimilarity(a, b) {
352
369
  let dot = 0;
@@ -362,11 +379,12 @@ function cosineSimilarity(a, b) {
362
379
  return dot / denom;
363
380
  }
364
381
  async function semanticSearch(query, limit = 10) {
365
- if (!db) {
382
+ const db4 = getDb();
383
+ if (!db4) {
366
384
  throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
367
385
  }
368
386
  const queryEmbedding = await embedText(query);
369
- const rows = db.prepare("SELECT path, embedding FROM note_embeddings").all();
387
+ const rows = db4.prepare("SELECT path, embedding FROM note_embeddings").all();
370
388
  const scored = [];
371
389
  for (const row of rows) {
372
390
  const noteEmbedding = new Float32Array(
@@ -382,10 +400,11 @@ async function semanticSearch(query, limit = 10) {
382
400
  return scored.slice(0, limit);
383
401
  }
384
402
  async function findSemanticallySimilar(sourcePath, limit = 10, excludePaths) {
385
- if (!db) {
403
+ const db4 = getDb();
404
+ if (!db4) {
386
405
  throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
387
406
  }
388
- const sourceRow = db.prepare("SELECT embedding FROM note_embeddings WHERE path = ?").get(sourcePath);
407
+ const sourceRow = db4.prepare("SELECT embedding FROM note_embeddings WHERE path = ?").get(sourcePath);
389
408
  if (!sourceRow) {
390
409
  return [];
391
410
  }
@@ -394,7 +413,7 @@ async function findSemanticallySimilar(sourcePath, limit = 10, excludePaths) {
394
413
  sourceRow.embedding.byteOffset,
395
414
  sourceRow.embedding.byteLength / Float32Array.BYTES_PER_ELEMENT
396
415
  );
397
- const rows = db.prepare("SELECT path, embedding FROM note_embeddings WHERE path != ?").all(sourcePath);
416
+ const rows = db4.prepare("SELECT path, embedding FROM note_embeddings WHERE path != ?").all(sourcePath);
398
417
  const scored = [];
399
418
  for (const row of rows) {
400
419
  if (excludePaths?.has(row.path)) continue;
@@ -430,12 +449,13 @@ function setEmbeddingsBuilding(value) {
430
449
  embeddingsBuilding = value;
431
450
  }
432
451
  function hasEmbeddingsIndex() {
433
- if (!db) return false;
452
+ const db4 = getDb();
453
+ if (!db4) return false;
434
454
  try {
435
455
  const state2 = getEmbeddingsBuildState();
436
456
  if (state2 === "complete") return true;
437
457
  if (state2 === "none") {
438
- const row = db.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
458
+ const row = db4.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
439
459
  return row.count > 0;
440
460
  }
441
461
  return false;
@@ -444,9 +464,10 @@ function hasEmbeddingsIndex() {
444
464
  }
445
465
  }
446
466
  function getStoredEmbeddingModel() {
447
- if (!db) return null;
467
+ const db4 = getDb();
468
+ if (!db4) return null;
448
469
  try {
449
- const row = db.prepare("SELECT model FROM note_embeddings LIMIT 1").get();
470
+ const row = db4.prepare("SELECT model FROM note_embeddings LIMIT 1").get();
450
471
  return row?.model ?? null;
451
472
  } catch {
452
473
  return null;
@@ -458,19 +479,21 @@ function needsEmbeddingRebuild() {
458
479
  return stored !== activeModelConfig.id;
459
480
  }
460
481
  function getEmbeddingsCount() {
461
- if (!db) return 0;
482
+ const db4 = getDb();
483
+ if (!db4) return 0;
462
484
  try {
463
- const row = db.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
485
+ const row = db4.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
464
486
  return row.count;
465
487
  } catch {
466
488
  return 0;
467
489
  }
468
490
  }
469
491
  function loadAllNoteEmbeddings() {
492
+ const db4 = getDb();
470
493
  const result = /* @__PURE__ */ new Map();
471
- if (!db) return result;
494
+ if (!db4) return result;
472
495
  try {
473
- const rows = db.prepare("SELECT path, embedding FROM note_embeddings").all();
496
+ const rows = db4.prepare("SELECT path, embedding FROM note_embeddings").all();
474
497
  for (const row of rows) {
475
498
  const embedding = new Float32Array(
476
499
  row.embedding.buffer,
@@ -500,17 +523,18 @@ function buildEntityEmbeddingText(entity, vaultPath2) {
500
523
  return parts.join(" ");
501
524
  }
502
525
  async function buildEntityEmbeddingsIndex(vaultPath2, entities, onProgress) {
503
- if (!db) {
526
+ const db4 = getDb();
527
+ if (!db4) {
504
528
  throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
505
529
  }
506
530
  await initEmbeddings();
507
531
  setEmbeddingsBuildState("building_entities");
508
532
  const existingHashes = /* @__PURE__ */ new Map();
509
- const rows = db.prepare("SELECT entity_name, source_hash FROM entity_embeddings").all();
533
+ const rows = db4.prepare("SELECT entity_name, source_hash FROM entity_embeddings").all();
510
534
  for (const row of rows) {
511
535
  existingHashes.set(row.entity_name, row.source_hash);
512
536
  }
513
- const upsert = db.prepare(`
537
+ const upsert = db4.prepare(`
514
538
  INSERT OR REPLACE INTO entity_embeddings (entity_name, embedding, source_hash, model, updated_at)
515
539
  VALUES (?, ?, ?, ?, ?)
516
540
  `);
@@ -534,7 +558,7 @@ async function buildEntityEmbeddingsIndex(vaultPath2, entities, onProgress) {
534
558
  }
535
559
  if (onProgress) onProgress(done, total);
536
560
  }
537
- const deleteStmt = db.prepare("DELETE FROM entity_embeddings WHERE entity_name = ?");
561
+ const deleteStmt = db4.prepare("DELETE FROM entity_embeddings WHERE entity_name = ?");
538
562
  for (const existingName of existingHashes.keys()) {
539
563
  if (!entities.has(existingName)) {
540
564
  deleteStmt.run(existingName);
@@ -544,15 +568,16 @@ async function buildEntityEmbeddingsIndex(vaultPath2, entities, onProgress) {
544
568
  return updated;
545
569
  }
546
570
  async function updateEntityEmbedding(entityName, entity, vaultPath2) {
547
- if (!db) return;
571
+ const db4 = getDb();
572
+ if (!db4) return;
548
573
  try {
549
574
  const text = buildEntityEmbeddingText(entity, vaultPath2);
550
575
  const hash = contentHash(text);
551
- const existing = db.prepare("SELECT source_hash FROM entity_embeddings WHERE entity_name = ?").get(entityName);
576
+ const existing = db4.prepare("SELECT source_hash FROM entity_embeddings WHERE entity_name = ?").get(entityName);
552
577
  if (existing?.source_hash === hash) return;
553
578
  const embedding = await embedTextCached(text);
554
579
  const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
555
- db.prepare(`
580
+ db4.prepare(`
556
581
  INSERT OR REPLACE INTO entity_embeddings (entity_name, embedding, source_hash, model, updated_at)
557
582
  VALUES (?, ?, ?, ?, ?)
558
583
  `).run(entityName, buf, hash, activeModelConfig.id, Date.now());
@@ -562,7 +587,7 @@ async function updateEntityEmbedding(entityName, entity, vaultPath2) {
562
587
  }
563
588
  function findSemanticallySimilarEntities(queryEmbedding, limit, excludeEntities) {
564
589
  const scored = [];
565
- for (const [entityName, embedding] of entityEmbeddingsMap) {
590
+ for (const [entityName, embedding] of getEmbMap()) {
566
591
  if (excludeEntities?.has(entityName)) continue;
567
592
  const similarity = cosineSimilarity(queryEmbedding, embedding);
568
593
  scored.push({ entityName, similarity: Math.round(similarity * 1e3) / 1e3 });
@@ -571,12 +596,16 @@ function findSemanticallySimilarEntities(queryEmbedding, limit, excludeEntities)
571
596
  return scored.slice(0, limit);
572
597
  }
573
598
  function hasEntityEmbeddingsIndex() {
574
- return entityEmbeddingsMap.size > 0;
599
+ return getEmbMap().size > 0;
600
+ }
601
+ function getEntityEmbeddingsMap() {
602
+ return entityEmbeddingsMap;
575
603
  }
576
604
  function loadEntityEmbeddingsToMemory() {
577
- if (!db) return;
605
+ const db4 = getDb();
606
+ if (!db4) return;
578
607
  try {
579
- const rows = db.prepare("SELECT entity_name, embedding FROM entity_embeddings").all();
608
+ const rows = db4.prepare("SELECT entity_name, embedding FROM entity_embeddings").all();
580
609
  entityEmbeddingsMap.clear();
581
610
  for (const row of rows) {
582
611
  const embedding = new Float32Array(
@@ -593,10 +622,11 @@ function loadEntityEmbeddingsToMemory() {
593
622
  }
594
623
  }
595
624
  function loadNoteEmbeddingsForPaths(paths) {
625
+ const db4 = getDb();
596
626
  const result = /* @__PURE__ */ new Map();
597
- if (!db || paths.length === 0) return result;
627
+ if (!db4 || paths.length === 0) return result;
598
628
  try {
599
- const stmt = db.prepare("SELECT path, embedding FROM note_embeddings WHERE path = ?");
629
+ const stmt = db4.prepare("SELECT path, embedding FROM note_embeddings WHERE path = ?");
600
630
  for (const p of paths) {
601
631
  const row = stmt.get(p);
602
632
  if (row) {
@@ -613,12 +643,13 @@ function loadNoteEmbeddingsForPaths(paths) {
613
643
  return result;
614
644
  }
615
645
  function getEntityEmbedding(entityName) {
616
- return entityEmbeddingsMap.get(entityName) ?? null;
646
+ return getEmbMap().get(entityName) ?? null;
617
647
  }
618
648
  function getEntityEmbeddingsCount() {
619
- if (!db) return 0;
649
+ const db4 = getDb();
650
+ if (!db4) return 0;
620
651
  try {
621
- const row = db.prepare("SELECT COUNT(*) as count FROM entity_embeddings").get();
652
+ const row = db4.prepare("SELECT COUNT(*) as count FROM entity_embeddings").get();
622
653
  return row.count;
623
654
  } catch {
624
655
  return 0;
@@ -1828,6 +1859,9 @@ import {
1828
1859
  recordEntityMention,
1829
1860
  getAllRecency as getAllRecencyFromDb
1830
1861
  } from "@velvetmonkey/vault-core";
1862
+ function getStateDb() {
1863
+ return getActiveScopeOrNull()?.stateDb ?? moduleStateDb3;
1864
+ }
1831
1865
  function setRecencyStateDb(stateDb2) {
1832
1866
  moduleStateDb3 = stateDb2;
1833
1867
  }
@@ -1898,9 +1932,10 @@ function getRecencyBoost(entityName, index) {
1898
1932
  return 0;
1899
1933
  }
1900
1934
  function loadRecencyFromStateDb() {
1901
- if (!moduleStateDb3) return null;
1935
+ const stateDb2 = getStateDb();
1936
+ if (!stateDb2) return null;
1902
1937
  try {
1903
- const rows = getAllRecencyFromDb(moduleStateDb3);
1938
+ const rows = getAllRecencyFromDb(stateDb2);
1904
1939
  if (rows.length === 0) return null;
1905
1940
  const lastMentioned = /* @__PURE__ */ new Map();
1906
1941
  let maxTime = 0;
@@ -1920,16 +1955,17 @@ function loadRecencyFromStateDb() {
1920
1955
  }
1921
1956
  }
1922
1957
  function saveRecencyToStateDb(index) {
1923
- if (!moduleStateDb3) {
1924
- console.error("[Flywheel] saveRecencyToStateDb: No StateDb available (moduleStateDb is null)");
1958
+ const stateDb2 = getStateDb();
1959
+ if (!stateDb2) {
1960
+ console.error("[Flywheel] saveRecencyToStateDb: No StateDb available");
1925
1961
  return;
1926
1962
  }
1927
1963
  console.error(`[Flywheel] saveRecencyToStateDb: Saving ${index.lastMentioned.size} entries...`);
1928
1964
  try {
1929
1965
  for (const [entityNameLower, timestamp] of index.lastMentioned) {
1930
- recordEntityMention(moduleStateDb3, entityNameLower, new Date(timestamp));
1966
+ recordEntityMention(stateDb2, entityNameLower, new Date(timestamp));
1931
1967
  }
1932
- const count = moduleStateDb3.db.prepare("SELECT COUNT(*) as cnt FROM recency").get();
1968
+ const count = stateDb2.db.prepare("SELECT COUNT(*) as cnt FROM recency").get();
1933
1969
  console.error(`[Flywheel] Saved recency: ${index.lastMentioned.size} entries \u2192 ${count.cnt} rows in table`);
1934
1970
  } catch (e) {
1935
1971
  console.error("[Flywheel] Failed to save recency to StateDb:", e);
@@ -1939,6 +1975,7 @@ var moduleStateDb3, RECENCY_CACHE_VERSION, EXCLUDED_FOLDERS;
1939
1975
  var init_recency = __esm({
1940
1976
  "src/core/shared/recency.ts"() {
1941
1977
  "use strict";
1978
+ init_vault_scope();
1942
1979
  moduleStateDb3 = null;
1943
1980
  RECENCY_CACHE_VERSION = 1;
1944
1981
  EXCLUDED_FOLDERS = /* @__PURE__ */ new Set([
@@ -2958,6 +2995,114 @@ var init_cooccurrence = __esm({
2958
2995
  }
2959
2996
  });
2960
2997
 
2998
+ // src/core/shared/retrievalCooccurrence.ts
2999
+ function mineRetrievalCooccurrence(stateDb2) {
3000
+ const db4 = stateDb2.db;
3001
+ const lastRow = db4.prepare(
3002
+ `SELECT value FROM fts_metadata WHERE key = ?`
3003
+ ).get(LAST_PROCESSED_KEY);
3004
+ const lastId = lastRow ? parseInt(lastRow.value, 10) : 0;
3005
+ const toolPlaceholders = Array.from(RETRIEVAL_TOOLS).map(() => "?").join(",");
3006
+ const rows = db4.prepare(`
3007
+ SELECT id, session_id, note_paths, timestamp
3008
+ FROM tool_invocations
3009
+ WHERE id > ? AND tool_name IN (${toolPlaceholders}) AND note_paths IS NOT NULL AND session_id IS NOT NULL
3010
+ ORDER BY id
3011
+ `).all(lastId, ...RETRIEVAL_TOOLS);
3012
+ if (rows.length === 0) return 0;
3013
+ const sessionNotes = /* @__PURE__ */ new Map();
3014
+ for (const row of rows) {
3015
+ let paths;
3016
+ try {
3017
+ paths = JSON.parse(row.note_paths);
3018
+ } catch {
3019
+ continue;
3020
+ }
3021
+ const existing = sessionNotes.get(row.session_id);
3022
+ if (existing) {
3023
+ for (const p of paths) existing.paths.add(p);
3024
+ existing.timestamp = Math.max(existing.timestamp, row.timestamp);
3025
+ } else {
3026
+ sessionNotes.set(row.session_id, {
3027
+ paths: new Set(paths),
3028
+ timestamp: row.timestamp
3029
+ });
3030
+ }
3031
+ }
3032
+ const insertStmt = db4.prepare(`
3033
+ INSERT OR IGNORE INTO retrieval_cooccurrence (note_a, note_b, session_id, timestamp, weight)
3034
+ VALUES (?, ?, ?, ?, ?)
3035
+ `);
3036
+ let inserted = 0;
3037
+ const maxId = rows[rows.length - 1].id;
3038
+ const insertAll = db4.transaction(() => {
3039
+ for (const [sessionId, { paths, timestamp }] of sessionNotes) {
3040
+ const filtered = Array.from(paths).filter((p) => !DAILY_NOTE_RE.test(p));
3041
+ if (filtered.length < 2) continue;
3042
+ const weight = 1 / Math.log(filtered.length);
3043
+ for (let i = 0; i < filtered.length; i++) {
3044
+ for (let j = i + 1; j < filtered.length; j++) {
3045
+ const [a, b] = filtered[i] < filtered[j] ? [filtered[i], filtered[j]] : [filtered[j], filtered[i]];
3046
+ const result = insertStmt.run(a, b, sessionId, timestamp, weight);
3047
+ if (result.changes > 0) inserted++;
3048
+ }
3049
+ }
3050
+ }
3051
+ db4.prepare(
3052
+ `INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)`
3053
+ ).run(LAST_PROCESSED_KEY, String(maxId));
3054
+ });
3055
+ insertAll();
3056
+ return inserted;
3057
+ }
3058
+ function buildRetrievalBoostMap(seedNotePaths, stateDb2) {
3059
+ if (seedNotePaths.size === 0) return /* @__PURE__ */ new Map();
3060
+ const now = Date.now();
3061
+ const halfLifeMs = HALF_LIFE_DAYS * 24 * 60 * 60 * 1e3;
3062
+ const lambda = Math.LN2 / halfLifeMs;
3063
+ const boostMap = /* @__PURE__ */ new Map();
3064
+ for (const seedPath of seedNotePaths) {
3065
+ const rows = stateDb2.db.prepare(`
3066
+ SELECT note_a, note_b, timestamp, weight
3067
+ FROM retrieval_cooccurrence
3068
+ WHERE note_a = ? OR note_b = ?
3069
+ `).all(seedPath, seedPath);
3070
+ for (const row of rows) {
3071
+ const otherNote = row.note_a === seedPath ? row.note_b : row.note_a;
3072
+ if (seedNotePaths.has(otherNote)) continue;
3073
+ const age = now - row.timestamp;
3074
+ const decayFactor = Math.exp(-lambda * age);
3075
+ const w = row.weight * decayFactor;
3076
+ boostMap.set(otherNote, (boostMap.get(otherNote) || 0) + w);
3077
+ }
3078
+ }
3079
+ return boostMap;
3080
+ }
3081
+ function getRetrievalBoost(entityPath, retrievalBoostMap) {
3082
+ if (!entityPath || retrievalBoostMap.size === 0) return 0;
3083
+ const weight = retrievalBoostMap.get(entityPath) || 0;
3084
+ if (weight <= 0) return 0;
3085
+ return Math.min(Math.round(weight * 3), MAX_RETRIEVAL_BOOST);
3086
+ }
3087
+ function pruneStaleRetrievalCooccurrence(stateDb2, maxAgeDays = 30) {
3088
+ const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1e3;
3089
+ const result = stateDb2.db.prepare(
3090
+ "DELETE FROM retrieval_cooccurrence WHERE timestamp < ?"
3091
+ ).run(cutoff);
3092
+ return result.changes;
3093
+ }
3094
+ var MAX_RETRIEVAL_BOOST, HALF_LIFE_DAYS, LAST_PROCESSED_KEY, DAILY_NOTE_RE, RETRIEVAL_TOOLS;
3095
+ var init_retrievalCooccurrence = __esm({
3096
+ "src/core/shared/retrievalCooccurrence.ts"() {
3097
+ "use strict";
3098
+ MAX_RETRIEVAL_BOOST = 6;
3099
+ HALF_LIFE_DAYS = 7;
3100
+ LAST_PROCESSED_KEY = "retrieval_cooc_last_id";
3101
+ DAILY_NOTE_RE = /\d{4}-\d{2}-\d{2}\.md$/;
3102
+ RETRIEVAL_TOOLS = /* @__PURE__ */ new Set(["recall", "search", "search_notes"]);
3103
+ }
3104
+ });
3105
+
2961
3106
  // src/core/write/edgeWeights.ts
2962
3107
  function setEdgeWeightStateDb(stateDb2) {
2963
3108
  moduleStateDb4 = stateDb2;
@@ -3179,7 +3324,7 @@ function setWriteStateDb(stateDb2) {
3179
3324
  setRecencyStateDb(stateDb2);
3180
3325
  }
3181
3326
  function getWriteStateDb() {
3182
- return moduleStateDb5;
3327
+ return getActiveScopeOrNull()?.stateDb ?? moduleStateDb5;
3183
3328
  }
3184
3329
  function setWikilinkConfig(config) {
3185
3330
  moduleConfig = config;
@@ -3713,6 +3858,21 @@ async function suggestRelatedLinks(content, options = {}) {
3713
3858
  if (displayName) cooccurrenceSeeds.add(displayName);
3714
3859
  }
3715
3860
  }
3861
+ const stateDb2 = getWriteStateDb();
3862
+ let retrievalBoostMap = /* @__PURE__ */ new Map();
3863
+ if (!disabled.has("cooccurrence") && stateDb2 && cooccurrenceSeeds.size > 0) {
3864
+ const seedNotePaths = /* @__PURE__ */ new Set();
3865
+ for (const seedName of cooccurrenceSeeds) {
3866
+ const seedEntity = entitiesWithTypes.find((e) => e.entity.name === seedName);
3867
+ if (seedEntity?.entity.path) seedNotePaths.add(seedEntity.entity.path);
3868
+ }
3869
+ if (seedNotePaths.size > 0) {
3870
+ try {
3871
+ retrievalBoostMap = buildRetrievalBoostMap(seedNotePaths, stateDb2);
3872
+ } catch {
3873
+ }
3874
+ }
3875
+ }
3716
3876
  if (!disabled.has("cooccurrence") && cooccurrenceIndex && cooccurrenceSeeds.size > 0) {
3717
3877
  for (const { entity, category } of entitiesWithTypes) {
3718
3878
  const entityName = entity.name;
@@ -3720,7 +3880,9 @@ async function suggestRelatedLinks(content, options = {}) {
3720
3880
  if (!disabled.has("length_filter") && entityName.length > MAX_ENTITY_LENGTH) continue;
3721
3881
  if (!disabled.has("article_filter") && isLikelyArticleTitle(entityName)) continue;
3722
3882
  if (linkedEntities.has(entityName.toLowerCase())) continue;
3723
- const boost = getCooccurrenceBoost(entityName, cooccurrenceSeeds, cooccurrenceIndex, recencyIndex);
3883
+ const contentCoocBoost = getCooccurrenceBoost(entityName, cooccurrenceSeeds, cooccurrenceIndex, recencyIndex);
3884
+ const retrievalCoocBoost = getRetrievalBoost(entity.path, retrievalBoostMap);
3885
+ const boost = Math.max(contentCoocBoost, retrievalCoocBoost);
3724
3886
  if (boost > 0) {
3725
3887
  const existing = scoredEntities.find((e) => e.name === entityName);
3726
3888
  if (existing) {
@@ -4082,6 +4244,7 @@ var init_wikilinks = __esm({
4082
4244
  init_recency();
4083
4245
  init_stemmer();
4084
4246
  init_cooccurrence();
4247
+ init_retrievalCooccurrence();
4085
4248
  init_recency();
4086
4249
  init_embeddings();
4087
4250
  init_edgeWeights();
@@ -5938,7 +6101,7 @@ var init_taskHelpers = __esm({
5938
6101
 
5939
6102
  // src/index.ts
5940
6103
  import * as path32 from "path";
5941
- import { readFileSync as readFileSync5, realpathSync } from "fs";
6104
+ import { readFileSync as readFileSync5, realpathSync, existsSync as existsSync3, statSync as statSync5 } from "fs";
5942
6105
  import { fileURLToPath } from "url";
5943
6106
  import { dirname as dirname6, join as join17 } from "path";
5944
6107
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -7505,6 +7668,7 @@ async function flushLogs() {
7505
7668
  // src/core/read/fts5.ts
7506
7669
  init_vault();
7507
7670
  init_serverLog();
7671
+ init_vault_scope();
7508
7672
  import * as fs7 from "fs";
7509
7673
  var EXCLUDED_DIRS3 = /* @__PURE__ */ new Set([
7510
7674
  ".obsidian",
@@ -7526,6 +7690,9 @@ function splitFrontmatter(raw) {
7526
7690
  return { frontmatter: values, body: raw.substring(end + 4) };
7527
7691
  }
7528
7692
  var db2 = null;
7693
+ function getDb2() {
7694
+ return getActiveScopeOrNull()?.stateDb?.db ?? db2;
7695
+ }
7529
7696
  var state = {
7530
7697
  ready: false,
7531
7698
  building: false,
@@ -7558,10 +7725,11 @@ function shouldIndexFile2(filePath) {
7558
7725
  return !parts.some((part) => EXCLUDED_DIRS3.has(part));
7559
7726
  }
7560
7727
  async function buildFTS5Index(vaultPath2) {
7728
+ const db4 = getDb2();
7561
7729
  try {
7562
7730
  state.error = null;
7563
7731
  state.building = true;
7564
- if (!db2) {
7732
+ if (!db4) {
7565
7733
  throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
7566
7734
  }
7567
7735
  const files = await scanVault(vaultPath2);
@@ -7581,16 +7749,16 @@ async function buildFTS5Index(vaultPath2) {
7581
7749
  serverLog("fts5", `Skipping ${file.path}: ${err}`, "warn");
7582
7750
  }
7583
7751
  }
7584
- const insert = db2.prepare(
7752
+ const insert = db4.prepare(
7585
7753
  "INSERT INTO notes_fts (path, title, frontmatter, content) VALUES (?, ?, ?, ?)"
7586
7754
  );
7587
7755
  const now = /* @__PURE__ */ new Date();
7588
- const swapAll = db2.transaction(() => {
7589
- db2.exec("DELETE FROM notes_fts");
7756
+ const swapAll = db4.transaction(() => {
7757
+ db4.exec("DELETE FROM notes_fts");
7590
7758
  for (const row of rows) {
7591
7759
  insert.run(...row);
7592
7760
  }
7593
- db2.prepare(
7761
+ db4.prepare(
7594
7762
  "INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
7595
7763
  ).run("last_built", now.toISOString());
7596
7764
  });
@@ -7617,11 +7785,12 @@ async function buildFTS5Index(vaultPath2) {
7617
7785
  }
7618
7786
  }
7619
7787
  function isIndexStale(_vaultPath) {
7620
- if (!db2) {
7788
+ const db4 = getDb2();
7789
+ if (!db4) {
7621
7790
  return true;
7622
7791
  }
7623
7792
  try {
7624
- const row = db2.prepare(
7793
+ const row = db4.prepare(
7625
7794
  "SELECT value FROM fts_metadata WHERE key = ?"
7626
7795
  ).get("last_built");
7627
7796
  if (!row) {
@@ -7634,12 +7803,19 @@ function isIndexStale(_vaultPath) {
7634
7803
  return true;
7635
7804
  }
7636
7805
  }
7806
+ function sanitizeFTS5Query(query) {
7807
+ if (!query?.trim()) return "";
7808
+ return query.replace(/"/g, '""').replace(/[(){}[\]^~:\-]/g, " ").replace(/\s+/g, " ").trim();
7809
+ }
7637
7810
  function searchFTS5(_vaultPath, query, limit = 10) {
7638
- if (!db2) {
7811
+ const db4 = getDb2();
7812
+ if (!db4) {
7639
7813
  throw new Error("FTS5 database not initialized. Call setFTS5Database() first.");
7640
7814
  }
7815
+ const sanitized = sanitizeFTS5Query(query);
7816
+ if (!sanitized) return [];
7641
7817
  try {
7642
- const stmt = db2.prepare(`
7818
+ const stmt = db4.prepare(`
7643
7819
  SELECT
7644
7820
  path,
7645
7821
  title,
@@ -7649,11 +7825,11 @@ function searchFTS5(_vaultPath, query, limit = 10) {
7649
7825
  ORDER BY bm25(notes_fts, 0.0, 5.0, 10.0, 1.0)
7650
7826
  LIMIT ?
7651
7827
  `);
7652
- const results = stmt.all(query, limit);
7828
+ const results = stmt.all(sanitized, limit);
7653
7829
  return results;
7654
7830
  } catch (err) {
7655
- if (err instanceof Error && err.message.includes("fts5: syntax error")) {
7656
- throw new Error(`Invalid search query: ${query}. Check FTS5 syntax.`);
7831
+ if (err instanceof Error && err.message.includes("fts5:")) {
7832
+ return [];
7657
7833
  }
7658
7834
  throw err;
7659
7835
  }
@@ -7662,9 +7838,10 @@ function getFTS5State() {
7662
7838
  return { ...state };
7663
7839
  }
7664
7840
  function getContentPreview(notePath, maxChars = 300) {
7665
- if (!db2) return null;
7841
+ const db4 = getDb2();
7842
+ if (!db4) return null;
7666
7843
  try {
7667
- const row = db2.prepare(
7844
+ const row = db4.prepare(
7668
7845
  "SELECT substr(content, 1, ?) as preview FROM notes_fts WHERE path = ?"
7669
7846
  ).get(maxChars + 50, notePath);
7670
7847
  if (!row?.preview) return null;
@@ -7675,9 +7852,10 @@ function getContentPreview(notePath, maxChars = 300) {
7675
7852
  }
7676
7853
  }
7677
7854
  function countFTS5Mentions(term) {
7678
- if (!db2) return 0;
7855
+ const db4 = getDb2();
7856
+ if (!db4) return 0;
7679
7857
  try {
7680
- const result = db2.prepare(
7858
+ const result = db4.prepare(
7681
7859
  "SELECT COUNT(*) as cnt FROM notes_fts WHERE content MATCH ?"
7682
7860
  ).get(`"${term}"`);
7683
7861
  return result?.cnt ?? 0;
@@ -7828,7 +8006,11 @@ async function getTasksWithDueDates(index, vaultPath2, options = {}) {
7828
8006
 
7829
8007
  // src/core/read/taskCache.ts
7830
8008
  init_serverLog();
8009
+ init_vault_scope();
7831
8010
  var db3 = null;
8011
+ function getDb3() {
8012
+ return getActiveScopeOrNull()?.stateDb?.db ?? db3;
8013
+ }
7832
8014
  var TASK_CACHE_STALE_MS = 30 * 60 * 1e3;
7833
8015
  var cacheReady = false;
7834
8016
  var rebuildInProgress = false;
@@ -7851,7 +8033,8 @@ function isTaskCacheBuilding() {
7851
8033
  return rebuildInProgress;
7852
8034
  }
7853
8035
  async function buildTaskCache(vaultPath2, index, excludeTags) {
7854
- if (!db3) {
8036
+ const db4 = getDb3();
8037
+ if (!db4) {
7855
8038
  throw new Error("Task cache database not initialized. Call setTaskCacheDatabase() first.");
7856
8039
  }
7857
8040
  if (rebuildInProgress) return;
@@ -7882,16 +8065,16 @@ async function buildTaskCache(vaultPath2, index, excludeTags) {
7882
8065
  ]);
7883
8066
  }
7884
8067
  }
7885
- const insertStmt = db3.prepare(`
8068
+ const insertStmt = db4.prepare(`
7886
8069
  INSERT OR REPLACE INTO tasks (path, line, text, status, raw, context, tags_json, due_date)
7887
8070
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
7888
8071
  `);
7889
- const swapAll = db3.transaction(() => {
7890
- db3.prepare("DELETE FROM tasks").run();
8072
+ const swapAll = db4.transaction(() => {
8073
+ db4.prepare("DELETE FROM tasks").run();
7891
8074
  for (const row of allRows) {
7892
8075
  insertStmt.run(...row);
7893
8076
  }
7894
- db3.prepare(
8077
+ db4.prepare(
7895
8078
  "INSERT OR REPLACE INTO fts_metadata (key, value) VALUES (?, ?)"
7896
8079
  ).run("task_cache_built", (/* @__PURE__ */ new Date()).toISOString());
7897
8080
  });
@@ -7904,16 +8087,17 @@ async function buildTaskCache(vaultPath2, index, excludeTags) {
7904
8087
  }
7905
8088
  }
7906
8089
  async function updateTaskCacheForFile(vaultPath2, relativePath) {
7907
- if (!db3) return;
7908
- db3.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
8090
+ const db4 = getDb3();
8091
+ if (!db4) return;
8092
+ db4.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
7909
8093
  const absolutePath = path12.join(vaultPath2, relativePath);
7910
8094
  const tasks = await extractTasksFromNote(relativePath, absolutePath);
7911
8095
  if (tasks.length > 0) {
7912
- const insertStmt = db3.prepare(`
8096
+ const insertStmt = db4.prepare(`
7913
8097
  INSERT OR REPLACE INTO tasks (path, line, text, status, raw, context, tags_json, due_date)
7914
8098
  VALUES (?, ?, ?, ?, ?, ?, ?, ?)
7915
8099
  `);
7916
- const insertBatch = db3.transaction(() => {
8100
+ const insertBatch = db4.transaction(() => {
7917
8101
  for (const task of tasks) {
7918
8102
  insertStmt.run(
7919
8103
  task.path,
@@ -7931,11 +8115,13 @@ async function updateTaskCacheForFile(vaultPath2, relativePath) {
7931
8115
  }
7932
8116
  }
7933
8117
  function removeTaskCacheForFile(relativePath) {
7934
- if (!db3) return;
7935
- db3.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
8118
+ const db4 = getDb3();
8119
+ if (!db4) return;
8120
+ db4.prepare("DELETE FROM tasks WHERE path = ?").run(relativePath);
7936
8121
  }
7937
8122
  function queryTasksFromCache(options) {
7938
- if (!db3) {
8123
+ const db4 = getDb3();
8124
+ if (!db4) {
7939
8125
  throw new Error("Task cache database not initialized.");
7940
8126
  }
7941
8127
  const { status, folder, tag, excludeTags = [], has_due_date, limit, offset = 0 } = options;
@@ -7974,7 +8160,7 @@ function queryTasksFromCache(options) {
7974
8160
  countParams.push(...excludeTags);
7975
8161
  }
7976
8162
  const countWhere = countConditions.length > 0 ? "WHERE " + countConditions.join(" AND ") : "";
7977
- const countRows = db3.prepare(
8163
+ const countRows = db4.prepare(
7978
8164
  `SELECT status, COUNT(*) as cnt FROM tasks ${countWhere} GROUP BY status`
7979
8165
  ).all(...countParams);
7980
8166
  let openCount = 0;
@@ -7999,7 +8185,7 @@ function queryTasksFromCache(options) {
7999
8185
  limitClause = " LIMIT ? OFFSET ?";
8000
8186
  queryParams.push(limit, offset);
8001
8187
  }
8002
- const rows = db3.prepare(
8188
+ const rows = db4.prepare(
8003
8189
  `SELECT path, line, text, status, raw, context, tags_json, due_date FROM tasks ${whereClause} ${orderBy}${limitClause}`
8004
8190
  ).all(...queryParams);
8005
8191
  const tasks = rows.map((row) => ({
@@ -8021,9 +8207,10 @@ function queryTasksFromCache(options) {
8021
8207
  };
8022
8208
  }
8023
8209
  function isTaskCacheStale() {
8024
- if (!db3) return true;
8210
+ const db4 = getDb3();
8211
+ if (!db4) return true;
8025
8212
  try {
8026
- const row = db3.prepare(
8213
+ const row = db4.prepare(
8027
8214
  "SELECT value FROM fts_metadata WHERE key = ?"
8028
8215
  ).get("task_cache_built");
8029
8216
  if (!row) return true;
@@ -8372,7 +8559,7 @@ var GetBacklinksOutputSchema = {
8372
8559
  returned_count: z.coerce.number().describe("Number of backlinks returned (may be limited)"),
8373
8560
  backlinks: z.array(BacklinkItemSchema).describe("List of backlinks")
8374
8561
  };
8375
- function registerGraphTools(server2, getIndex, getVaultPath, getStateDb) {
8562
+ function registerGraphTools(server2, getIndex, getVaultPath, getStateDb2) {
8376
8563
  server2.registerTool(
8377
8564
  "get_backlinks",
8378
8565
  {
@@ -8503,7 +8690,7 @@ function registerGraphTools(server2, getIndex, getVaultPath, getStateDb) {
8503
8690
  limit: z.number().default(20).describe("Maximum number of results to return")
8504
8691
  },
8505
8692
  async ({ path: notePath, min_weight, limit: requestedLimit }) => {
8506
- const stateDb2 = getStateDb?.();
8693
+ const stateDb2 = getStateDb2?.();
8507
8694
  if (!stateDb2) {
8508
8695
  return { content: [{ type: "text", text: JSON.stringify({ error: "StateDb not initialized" }) }] };
8509
8696
  }
@@ -8546,7 +8733,7 @@ function registerGraphTools(server2, getIndex, getVaultPath, getStateDb) {
8546
8733
  limit: z.number().default(20).describe("Maximum number of results to return")
8547
8734
  },
8548
8735
  async ({ path: notePath, limit: requestedLimit }) => {
8549
- const stateDb2 = getStateDb?.();
8736
+ const stateDb2 = getStateDb2?.();
8550
8737
  if (!stateDb2) {
8551
8738
  return { content: [{ type: "text", text: JSON.stringify({ error: "StateDb not initialized" }) }] };
8552
8739
  }
@@ -8667,7 +8854,7 @@ function findEntityMatches(text, entities) {
8667
8854
  }
8668
8855
  return matches.sort((a, b) => a.start - b.start);
8669
8856
  }
8670
- function registerWikilinkTools(server2, getIndex, getVaultPath, getStateDb = () => null) {
8857
+ function registerWikilinkTools(server2, getIndex, getVaultPath, getStateDb2 = () => null) {
8671
8858
  const SuggestionSchema = z2.object({
8672
8859
  entity: z2.string().describe("The matched text in the input"),
8673
8860
  start: z2.coerce.number().describe("Start position in text (0-indexed)"),
@@ -8809,7 +8996,7 @@ function registerWikilinkTools(server2, getIndex, getVaultPath, getStateDb = ()
8809
8996
  strictness: "balanced"
8810
8997
  });
8811
8998
  if (scored.detailed) {
8812
- const stateDb2 = getStateDb();
8999
+ const stateDb2 = getStateDb2();
8813
9000
  if (stateDb2) {
8814
9001
  try {
8815
9002
  const weightedStats = getWeightedEntityStats(stateDb2);
@@ -9600,7 +9787,7 @@ function getSweepResults() {
9600
9787
  init_wikilinkFeedback();
9601
9788
  init_embeddings();
9602
9789
  var STALE_THRESHOLD_SECONDS = 300;
9603
- function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb = () => null, getWatcherStatus2 = () => null) {
9790
+ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb2 = () => null, getWatcherStatus2 = () => null) {
9604
9791
  const IndexProgressSchema = z3.object({
9605
9792
  parsed: z3.coerce.number().describe("Number of files parsed so far"),
9606
9793
  total: z3.coerce.number().describe("Total number of files to parse")
@@ -9721,6 +9908,21 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
9721
9908
  vaultAccessible = false;
9722
9909
  recommendations.push("Vault path is not accessible. Check PROJECT_PATH environment variable.");
9723
9910
  }
9911
+ let dbIntegrityFailed = false;
9912
+ const stateDb2 = getStateDb2();
9913
+ if (stateDb2) {
9914
+ try {
9915
+ const result = stateDb2.db.pragma("quick_check");
9916
+ const ok = result.length === 1 && Object.values(result[0])[0] === "ok";
9917
+ if (!ok) {
9918
+ dbIntegrityFailed = true;
9919
+ recommendations.push(`Database integrity check failed: ${Object.values(result[0])[0] ?? "unknown error"}`);
9920
+ }
9921
+ } catch (err) {
9922
+ dbIntegrityFailed = true;
9923
+ recommendations.push(`Database integrity check error: ${err instanceof Error ? err.message : err}`);
9924
+ }
9925
+ }
9724
9926
  const indexBuilt = indexState2 === "ready" && index !== void 0 && index.notes !== void 0;
9725
9927
  const indexAge = indexBuilt && index.builtAt ? Math.floor((Date.now() - index.builtAt.getTime()) / 1e3) : -1;
9726
9928
  const indexStale = indexBuilt && indexAge > STALE_THRESHOLD_SECONDS;
@@ -9744,7 +9946,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
9744
9946
  recommendations.push("No notes found in vault. Is PROJECT_PATH pointing to a markdown vault?");
9745
9947
  }
9746
9948
  let status;
9747
- if (!vaultAccessible || indexState2 === "error") {
9949
+ if (!vaultAccessible || indexState2 === "error" || dbIntegrityFailed) {
9748
9950
  status = "unhealthy";
9749
9951
  } else if (indexState2 === "building" || indexStale || recommendations.length > 0) {
9750
9952
  status = "degraded";
@@ -9769,7 +9971,6 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
9769
9971
  const config = getConfig2();
9770
9972
  const configInfo = Object.keys(config).length > 0 ? config : void 0;
9771
9973
  let lastRebuild;
9772
- const stateDb2 = getStateDb();
9773
9974
  if (stateDb2) {
9774
9975
  try {
9775
9976
  const events = getRecentIndexEvents(stateDb2, 1);
@@ -10071,7 +10272,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
10071
10272
  const checks = [];
10072
10273
  const index = getIndex();
10073
10274
  const vaultPath2 = getVaultPath();
10074
- const stateDb2 = getStateDb();
10275
+ const stateDb2 = getStateDb2();
10075
10276
  const watcherStatus = getWatcherStatus2();
10076
10277
  checks.push({
10077
10278
  name: "schema_version",
@@ -10517,6 +10718,7 @@ function enrichNoteResult(notePath, stateDb2, index) {
10517
10718
  }
10518
10719
 
10519
10720
  // src/tools/read/query.ts
10721
+ init_wikilinkFeedback();
10520
10722
  function matchesFrontmatter(note, where) {
10521
10723
  for (const [key, value] of Object.entries(where)) {
10522
10724
  const noteValue = note.frontmatter[key];
@@ -10584,7 +10786,7 @@ function sortNotes(notes, sortBy, order) {
10584
10786
  });
10585
10787
  return sorted;
10586
10788
  }
10587
- function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10789
+ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb2) {
10588
10790
  server2.tool(
10589
10791
  "search",
10590
10792
  'Search the vault \u2014 always start with just a query, no filters. Top results get full metadata (frontmatter, top backlinks/outlinks ranked by edge weight + recency); remaining results get lightweight summaries. Narrow with filters only if the broad search returns too many irrelevant results. Use get_note_structure for headings/full structure, get_backlinks for complete backlink lists.\n\nSearches across content (FTS5 full-text + hybrid semantic), entities (people/projects/technologies), and metadata (frontmatter/tags/folders). Hybrid semantic results are automatically included when embeddings have been built (via init_semantic).\n\nExample: search({ query: "quarterly review", limit: 5 })\nExample: search({ where: { type: "project", status: "active" } })\n\nMulti-vault: when configured with multiple vaults, omitting the `vault` parameter searches all vaults and merges results (each result includes a `vault` field). Pass `vault` to search a specific vault.',
@@ -10618,7 +10820,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10618
10820
  const index = getIndex();
10619
10821
  const vaultPath2 = getVaultPath();
10620
10822
  if (prefix && query) {
10621
- const stateDb2 = getStateDb();
10823
+ const stateDb2 = getStateDb2();
10622
10824
  if (!stateDb2) {
10623
10825
  return { content: [{ type: "text", text: JSON.stringify({ results: [], count: 0, query, error: "StateDb not initialized" }, null, 2) }] };
10624
10826
  }
@@ -10666,7 +10868,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10666
10868
  matchingNotes = sortNotes(matchingNotes, sort_by ?? "modified", order ?? "desc");
10667
10869
  const totalMatches = matchingNotes.length;
10668
10870
  const limitedNotes = matchingNotes.slice(0, limit);
10669
- const stateDb2 = getStateDb();
10871
+ const stateDb2 = getStateDb2();
10670
10872
  const notes = limitedNotes.map(
10671
10873
  (note, i) => (i < detailN ? enrichResult : enrichResultLight)({ path: note.path, title: note.title }, index, stateDb2)
10672
10874
  );
@@ -10694,7 +10896,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10694
10896
  }
10695
10897
  const fts5Results = searchFTS5(vaultPath2, query, limit);
10696
10898
  let entityResults = [];
10697
- const stateDbEntity = getStateDb();
10899
+ const stateDbEntity = getStateDb2();
10698
10900
  if (stateDbEntity) {
10699
10901
  try {
10700
10902
  entityResults = searchEntities(stateDbEntity, query, limit);
@@ -10703,7 +10905,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10703
10905
  }
10704
10906
  let edgeRanked = [];
10705
10907
  if (context_note) {
10706
- const ctxStateDb = getStateDb();
10908
+ const ctxStateDb = getStateDb2();
10707
10909
  if (ctxStateDb) {
10708
10910
  try {
10709
10911
  const edgeRows = ctxStateDb.db.prepare(`
@@ -10768,7 +10970,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10768
10970
  }));
10769
10971
  scored.sort((a, b) => b.rrf_score - a.rrf_score);
10770
10972
  const filtered = applyFolderFilter(scored);
10771
- const stateDb2 = getStateDb();
10973
+ const stateDb2 = getStateDb2();
10772
10974
  const results = filtered.slice(0, limit).map((item, i) => ({
10773
10975
  ...(i < detailN ? enrichResult : enrichResultLight)({ path: item.path, title: item.title, snippet: item.snippet }, index, stateDb2),
10774
10976
  rrf_score: item.rrf_score,
@@ -10776,6 +10978,34 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10776
10978
  in_semantic: item.in_semantic,
10777
10979
  in_entity: item.in_entity
10778
10980
  }));
10981
+ if (stateDb2 && results.length < limit) {
10982
+ const existingPaths = new Set(results.map((r) => r.path));
10983
+ const backfill = [];
10984
+ for (const r of results.slice(0, 3)) {
10985
+ const rPath = r.path;
10986
+ if (!rPath) continue;
10987
+ try {
10988
+ const outlinks = getStoredNoteLinks(stateDb2, rPath);
10989
+ for (const target of outlinks) {
10990
+ const entityRow = stateDb2.db.prepare(
10991
+ "SELECT path FROM entities WHERE name_lower = ?"
10992
+ ).get(target);
10993
+ if (entityRow?.path && !existingPaths.has(entityRow.path)) {
10994
+ existingPaths.add(entityRow.path);
10995
+ backfill.push({
10996
+ ...enrichResultLight({ path: entityRow.path, title: target }, index, stateDb2),
10997
+ rrf_score: 0,
10998
+ in_fts5: false,
10999
+ in_semantic: false,
11000
+ in_entity: false
11001
+ });
11002
+ }
11003
+ }
11004
+ } catch {
11005
+ }
11006
+ }
11007
+ results.push(...backfill.slice(0, limit - results.length));
11008
+ }
10779
11009
  return { content: [{ type: "text", text: JSON.stringify({
10780
11010
  method: "hybrid",
10781
11011
  query,
@@ -10795,12 +11025,36 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10795
11025
  ...entityRanked.map((r) => ({ path: r.path, title: r.name, snippet: void 0, in_entity: true }))
10796
11026
  ];
10797
11027
  const filtered = applyFolderFilter(mergedItems);
10798
- const stateDb2 = getStateDb();
11028
+ const stateDb2 = getStateDb2();
10799
11029
  const sliced = filtered.slice(0, limit);
10800
11030
  const results = sliced.map((item, i) => ({
10801
11031
  ...(i < detailN ? enrichResult : enrichResultLight)({ path: item.path, title: item.title, snippet: item.snippet }, index, stateDb2),
10802
11032
  ..."in_fts5" in item ? { in_fts5: true } : { in_entity: true }
10803
11033
  }));
11034
+ if (stateDb2 && results.length < limit) {
11035
+ const existingPaths = new Set(results.map((r) => r.path));
11036
+ const backfill = [];
11037
+ for (const r of results.slice(0, 3)) {
11038
+ const rPath = r.path;
11039
+ if (!rPath) continue;
11040
+ try {
11041
+ const outlinks = getStoredNoteLinks(stateDb2, rPath);
11042
+ for (const target of outlinks) {
11043
+ const entityRow = stateDb2.db.prepare(
11044
+ "SELECT path FROM entities WHERE name_lower = ?"
11045
+ ).get(target);
11046
+ if (entityRow?.path && !existingPaths.has(entityRow.path)) {
11047
+ existingPaths.add(entityRow.path);
11048
+ backfill.push({
11049
+ ...enrichResultLight({ path: entityRow.path, title: target }, index, stateDb2)
11050
+ });
11051
+ }
11052
+ }
11053
+ } catch {
11054
+ }
11055
+ }
11056
+ results.push(...backfill.slice(0, limit - results.length));
11057
+ }
10804
11058
  return { content: [{ type: "text", text: JSON.stringify({
10805
11059
  method: "fts5",
10806
11060
  query,
@@ -10808,7 +11062,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10808
11062
  results
10809
11063
  }, null, 2) }] };
10810
11064
  }
10811
- const stateDbFts = getStateDb();
11065
+ const stateDbFts = getStateDb2();
10812
11066
  const fts5Filtered = applyFolderFilter(fts5Results);
10813
11067
  const enrichedFts5 = fts5Filtered.map((r, i) => ({ ...(i < detailN ? enrichResult : enrichResultLight)({ path: r.path, title: r.title, snippet: r.snippet }, index, stateDbFts), in_fts5: true }));
10814
11068
  return { content: [{ type: "text", text: JSON.stringify({
@@ -10896,7 +11150,7 @@ function suggestEntityAliases(stateDb2, folder) {
10896
11150
 
10897
11151
  // src/tools/read/system.ts
10898
11152
  init_edgeWeights();
10899
- function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb) {
11153
+ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb2) {
10900
11154
  const RefreshIndexOutputSchema = {
10901
11155
  success: z5.boolean().describe("Whether the refresh succeeded"),
10902
11156
  notes_count: z5.number().describe("Number of notes indexed"),
@@ -10922,7 +11176,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
10922
11176
  const newIndex = await buildVaultIndex(vaultPath2);
10923
11177
  setIndex(newIndex);
10924
11178
  setIndexState("ready");
10925
- const stateDb2 = getStateDb?.();
11179
+ const stateDb2 = getStateDb2?.();
10926
11180
  if (stateDb2) {
10927
11181
  try {
10928
11182
  const entityIndex2 = await scanVaultEntities2(vaultPath2, {
@@ -11009,7 +11263,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
11009
11263
  setIndexState("error");
11010
11264
  setIndexError(err instanceof Error ? err : new Error(String(err)));
11011
11265
  const duration = Date.now() - startTime;
11012
- const stateDb2 = getStateDb?.();
11266
+ const stateDb2 = getStateDb2?.();
11013
11267
  if (stateDb2) {
11014
11268
  recordIndexEvent(stateDb2, {
11015
11269
  trigger: "manual_refresh",
@@ -11267,7 +11521,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
11267
11521
  category,
11268
11522
  limit: perCategoryLimit
11269
11523
  }) => {
11270
- const stateDb2 = getStateDb?.();
11524
+ const stateDb2 = getStateDb2?.();
11271
11525
  if (!stateDb2) {
11272
11526
  return {
11273
11527
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
@@ -11322,7 +11576,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
11322
11576
  folder,
11323
11577
  limit: requestedLimit
11324
11578
  }) => {
11325
- const stateDb2 = getStateDb?.();
11579
+ const stateDb2 = getStateDb2?.();
11326
11580
  if (!stateDb2) {
11327
11581
  return {
11328
11582
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
@@ -11542,7 +11796,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
11542
11796
 
11543
11797
  // src/tools/read/primitives.ts
11544
11798
  import { getEntityByName as getEntityByName3 } from "@velvetmonkey/vault-core";
11545
- function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb = () => null) {
11799
+ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb2 = () => null) {
11546
11800
  server2.registerTool(
11547
11801
  "get_note_structure",
11548
11802
  {
@@ -11581,7 +11835,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
11581
11835
  enriched.backlink_count = backlinks.length;
11582
11836
  enriched.outlink_count = note.outlinks.length;
11583
11837
  }
11584
- const stateDb2 = getStateDb();
11838
+ const stateDb2 = getStateDb2();
11585
11839
  if (stateDb2 && note) {
11586
11840
  try {
11587
11841
  const entity = getEntityByName3(stateDb2, note.title);
@@ -12640,7 +12894,7 @@ function getExcludedPaths(index, config) {
12640
12894
  }
12641
12895
  return excluded;
12642
12896
  }
12643
- function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb, getConfig2) {
12897
+ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb2, getConfig2) {
12644
12898
  server2.registerTool(
12645
12899
  "graph_analysis",
12646
12900
  {
@@ -12810,7 +13064,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
12810
13064
  };
12811
13065
  }
12812
13066
  case "emerging_hubs": {
12813
- const db4 = getStateDb?.();
13067
+ const db4 = getStateDb2?.();
12814
13068
  if (!db4) {
12815
13069
  return {
12816
13070
  content: [{ type: "text", text: JSON.stringify({
@@ -17558,7 +17812,7 @@ function registerTagTools(server2, getIndex, getVaultPath) {
17558
17812
  // src/tools/write/wikilinkFeedback.ts
17559
17813
  init_wikilinkFeedback();
17560
17814
  import { z as z22 } from "zod";
17561
- function registerWikilinkFeedbackTools(server2, getStateDb) {
17815
+ function registerWikilinkFeedbackTools(server2, getStateDb2) {
17562
17816
  server2.registerTool(
17563
17817
  "wikilink_feedback",
17564
17818
  {
@@ -17579,7 +17833,7 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
17579
17833
  }
17580
17834
  },
17581
17835
  async ({ mode, entity, note_path, context, correct, limit, days_back, granularity, timestamp_before, timestamp_after, skip_status_update }) => {
17582
- const stateDb2 = getStateDb();
17836
+ const stateDb2 = getStateDb2();
17583
17837
  if (!stateDb2) {
17584
17838
  return {
17585
17839
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available \u2014 database not initialized yet" }) }],
@@ -17741,7 +17995,7 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
17741
17995
  // src/tools/write/corrections.ts
17742
17996
  init_corrections();
17743
17997
  import { z as z23 } from "zod";
17744
- function registerCorrectionTools(server2, getStateDb) {
17998
+ function registerCorrectionTools(server2, getStateDb2) {
17745
17999
  server2.tool(
17746
18000
  "vault_record_correction",
17747
18001
  'Record a persistent correction (e.g., "that link is wrong", "undo that"). Survives across sessions.',
@@ -17752,7 +18006,7 @@ function registerCorrectionTools(server2, getStateDb) {
17752
18006
  note_path: z23.string().optional().describe("Note path (if correction is about a specific note)")
17753
18007
  },
17754
18008
  async ({ correction_type, description, entity, note_path }) => {
17755
- const stateDb2 = getStateDb();
18009
+ const stateDb2 = getStateDb2();
17756
18010
  if (!stateDb2) {
17757
18011
  return {
17758
18012
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
@@ -17780,7 +18034,7 @@ function registerCorrectionTools(server2, getStateDb) {
17780
18034
  limit: z23.number().min(1).max(200).default(50).describe("Max entries to return")
17781
18035
  },
17782
18036
  async ({ status, entity, limit }) => {
17783
- const stateDb2 = getStateDb();
18037
+ const stateDb2 = getStateDb2();
17784
18038
  if (!stateDb2) {
17785
18039
  return {
17786
18040
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
@@ -17807,7 +18061,7 @@ function registerCorrectionTools(server2, getStateDb) {
17807
18061
  status: z23.enum(["applied", "dismissed"]).describe("New status")
17808
18062
  },
17809
18063
  async ({ correction_id, status }) => {
17810
- const stateDb2 = getStateDb();
18064
+ const stateDb2 = getStateDb2();
17811
18065
  if (!stateDb2) {
17812
18066
  return {
17813
18067
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
@@ -18155,7 +18409,7 @@ function findContradictions2(stateDb2, entity) {
18155
18409
  }
18156
18410
 
18157
18411
  // src/tools/write/memory.ts
18158
- function registerMemoryTools(server2, getStateDb) {
18412
+ function registerMemoryTools(server2, getStateDb2) {
18159
18413
  server2.tool(
18160
18414
  "memory",
18161
18415
  "Store, retrieve, search, and manage agent working memory. Actions: store, get, search, list, forget, summarize_session.",
@@ -18180,7 +18434,7 @@ function registerMemoryTools(server2, getStateDb) {
18180
18434
  tool_count: z24.number().optional().describe("Number of tool calls in session")
18181
18435
  },
18182
18436
  async (args) => {
18183
- const stateDb2 = getStateDb();
18437
+ const stateDb2 = getStateDb2();
18184
18438
  if (!stateDb2) {
18185
18439
  return {
18186
18440
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
@@ -18771,7 +19025,7 @@ async function performRecall(stateDb2, query, options = {}) {
18771
19025
  }
18772
19026
  return selected;
18773
19027
  }
18774
- function registerRecallTools(server2, getStateDb, getVaultPath, getIndex) {
19028
+ function registerRecallTools(server2, getStateDb2, getVaultPath, getIndex) {
18775
19029
  server2.tool(
18776
19030
  "recall",
18777
19031
  "Query everything the system knows about a topic. Searches across entities, notes, and memories with graph-boosted ranking.",
@@ -18784,7 +19038,7 @@ function registerRecallTools(server2, getStateDb, getVaultPath, getIndex) {
18784
19038
  diversity: z25.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
18785
19039
  },
18786
19040
  async (args) => {
18787
- const stateDb2 = getStateDb();
19041
+ const stateDb2 = getStateDb2();
18788
19042
  if (!stateDb2) {
18789
19043
  return {
18790
19044
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
@@ -18840,15 +19094,17 @@ import { z as z26 } from "zod";
18840
19094
  // src/core/shared/toolTracking.ts
18841
19095
  function recordToolInvocation(stateDb2, event) {
18842
19096
  stateDb2.db.prepare(
18843
- `INSERT INTO tool_invocations (timestamp, tool_name, session_id, note_paths, duration_ms, success)
18844
- VALUES (?, ?, ?, ?, ?, ?)`
19097
+ `INSERT INTO tool_invocations (timestamp, tool_name, session_id, note_paths, duration_ms, success, response_tokens, baseline_tokens)
19098
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
18845
19099
  ).run(
18846
19100
  Date.now(),
18847
19101
  event.tool_name,
18848
19102
  event.session_id ?? null,
18849
19103
  event.note_paths ? JSON.stringify(event.note_paths) : null,
18850
19104
  event.duration_ms ?? null,
18851
- event.success !== false ? 1 : 0
19105
+ event.success !== false ? 1 : 0,
19106
+ event.response_tokens ?? null,
19107
+ event.baseline_tokens ?? null
18852
19108
  );
18853
19109
  }
18854
19110
  function rowToInvocation(row) {
@@ -18859,7 +19115,9 @@ function rowToInvocation(row) {
18859
19115
  session_id: row.session_id,
18860
19116
  note_paths: row.note_paths ? JSON.parse(row.note_paths) : null,
18861
19117
  duration_ms: row.duration_ms,
18862
- success: row.success === 1
19118
+ success: row.success === 1,
19119
+ response_tokens: row.response_tokens,
19120
+ baseline_tokens: row.baseline_tokens
18863
19121
  };
18864
19122
  }
18865
19123
  function getToolUsageSummary(stateDb2, daysBack = 30) {
@@ -19119,7 +19377,7 @@ function buildVaultPulseSection(stateDb2) {
19119
19377
  estimated_tokens: estimateTokens2(content)
19120
19378
  };
19121
19379
  }
19122
- function registerBriefTools(server2, getStateDb) {
19380
+ function registerBriefTools(server2, getStateDb2) {
19123
19381
  server2.tool(
19124
19382
  "brief",
19125
19383
  "Get a startup context briefing: recent sessions, active entities, memories, pending corrections, and vault stats. Call at conversation start.",
@@ -19128,7 +19386,7 @@ function registerBriefTools(server2, getStateDb) {
19128
19386
  focus: z26.string().optional().describe("Focus entity or topic (filters content)")
19129
19387
  },
19130
19388
  async (args) => {
19131
- const stateDb2 = getStateDb();
19389
+ const stateDb2 = getStateDb2();
19132
19390
  if (!stateDb2) {
19133
19391
  return {
19134
19392
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
@@ -19189,7 +19447,7 @@ var VALID_CONFIG_KEYS = {
19189
19447
  implicit_patterns: z27.array(z27.string()),
19190
19448
  adaptive_strictness: z27.boolean()
19191
19449
  };
19192
- function registerConfigTools(server2, getConfig2, setConfig, getStateDb) {
19450
+ function registerConfigTools(server2, getConfig2, setConfig, getStateDb2) {
19193
19451
  server2.registerTool(
19194
19452
  "flywheel_config",
19195
19453
  {
@@ -19231,7 +19489,7 @@ function registerConfigTools(server2, getConfig2, setConfig, getStateDb) {
19231
19489
  }) }]
19232
19490
  };
19233
19491
  }
19234
- const stateDb2 = getStateDb();
19492
+ const stateDb2 = getStateDb2();
19235
19493
  if (!stateDb2) {
19236
19494
  return {
19237
19495
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
@@ -19553,7 +19811,7 @@ async function executeEnrich(stateDb2, vaultPath2, dryRun, batchSize, offset) {
19553
19811
  total_enriched: currentTotal
19554
19812
  };
19555
19813
  }
19556
- function registerInitTools(server2, getVaultPath, getStateDb) {
19814
+ function registerInitTools(server2, getVaultPath, getStateDb2) {
19557
19815
  server2.tool(
19558
19816
  "vault_init",
19559
19817
  `Initialize vault for Flywheel. Modes: "status" (check what's ready/missing), "run" (execute missing init steps), "enrich" (scan notes with zero wikilinks and apply entity links).`,
@@ -19564,7 +19822,7 @@ function registerInitTools(server2, getVaultPath, getStateDb) {
19564
19822
  offset: z28.number().default(0).describe("For enrich mode: skip this many eligible notes (for pagination)")
19565
19823
  },
19566
19824
  async ({ mode, dry_run, batch_size, offset }) => {
19567
- const stateDb2 = getStateDb();
19825
+ const stateDb2 = getStateDb2();
19568
19826
  const vaultPath2 = getVaultPath();
19569
19827
  switch (mode) {
19570
19828
  case "status": {
@@ -19748,7 +20006,7 @@ function purgeOldMetrics(stateDb2, retentionDays = 90) {
19748
20006
  }
19749
20007
 
19750
20008
  // src/tools/read/metrics.ts
19751
- function registerMetricsTools(server2, getIndex, getStateDb) {
20009
+ function registerMetricsTools(server2, getIndex, getStateDb2) {
19752
20010
  server2.registerTool(
19753
20011
  "vault_growth",
19754
20012
  {
@@ -19763,7 +20021,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
19763
20021
  },
19764
20022
  async ({ mode, metric, days_back, limit: eventLimit }) => {
19765
20023
  const index = getIndex();
19766
- const stateDb2 = getStateDb();
20024
+ const stateDb2 = getStateDb2();
19767
20025
  const daysBack = days_back ?? 30;
19768
20026
  let result;
19769
20027
  switch (mode) {
@@ -19832,7 +20090,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
19832
20090
 
19833
20091
  // src/tools/read/activity.ts
19834
20092
  import { z as z30 } from "zod";
19835
- function registerActivityTools(server2, getStateDb, getSessionId2) {
20093
+ function registerActivityTools(server2, getStateDb2, getSessionId2) {
19836
20094
  server2.registerTool(
19837
20095
  "vault_activity",
19838
20096
  {
@@ -19846,7 +20104,7 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
19846
20104
  }
19847
20105
  },
19848
20106
  async ({ mode, session_id, days_back, limit: resultLimit }) => {
19849
- const stateDb2 = getStateDb();
20107
+ const stateDb2 = getStateDb2();
19850
20108
  if (!stateDb2) {
19851
20109
  return {
19852
20110
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
@@ -20181,7 +20439,7 @@ async function findHybridSimilarNotes(db4, vaultPath2, index, sourcePath, option
20181
20439
 
20182
20440
  // src/tools/read/similarity.ts
20183
20441
  init_embeddings();
20184
- function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
20442
+ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb2) {
20185
20443
  server2.registerTool(
20186
20444
  "find_similar",
20187
20445
  {
@@ -20196,7 +20454,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
20196
20454
  async ({ path: path33, limit, diversity }) => {
20197
20455
  const index = getIndex();
20198
20456
  const vaultPath2 = getVaultPath();
20199
- const stateDb2 = getStateDb();
20457
+ const stateDb2 = getStateDb2();
20200
20458
  if (!stateDb2) {
20201
20459
  return {
20202
20460
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
@@ -20237,7 +20495,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
20237
20495
  init_embeddings();
20238
20496
  import { z as z32 } from "zod";
20239
20497
  import { getAllEntitiesFromDb } from "@velvetmonkey/vault-core";
20240
- function registerSemanticTools(server2, getVaultPath, getStateDb) {
20498
+ function registerSemanticTools(server2, getVaultPath, getStateDb2) {
20241
20499
  server2.registerTool(
20242
20500
  "init_semantic",
20243
20501
  {
@@ -20250,7 +20508,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
20250
20508
  }
20251
20509
  },
20252
20510
  async ({ force }) => {
20253
- const stateDb2 = getStateDb();
20511
+ const stateDb2 = getStateDb2();
20254
20512
  if (!stateDb2) {
20255
20513
  return {
20256
20514
  content: [{
@@ -20344,7 +20602,7 @@ import { getAllEntitiesFromDb as getAllEntitiesFromDb2, getDismissedMergePairs,
20344
20602
  function normalizeName(name) {
20345
20603
  return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
20346
20604
  }
20347
- function registerMergeTools2(server2, getStateDb) {
20605
+ function registerMergeTools2(server2, getStateDb2) {
20348
20606
  server2.tool(
20349
20607
  "suggest_entity_merges",
20350
20608
  "Find potential duplicate entities that could be merged based on name similarity",
@@ -20352,7 +20610,7 @@ function registerMergeTools2(server2, getStateDb) {
20352
20610
  limit: z33.number().optional().default(50).describe("Maximum number of suggestions to return")
20353
20611
  },
20354
20612
  async ({ limit }) => {
20355
- const stateDb2 = getStateDb();
20613
+ const stateDb2 = getStateDb2();
20356
20614
  if (!stateDb2) {
20357
20615
  return {
20358
20616
  content: [{ type: "text", text: JSON.stringify({ suggestions: [], error: "StateDb not available" }) }]
@@ -20458,7 +20716,7 @@ function registerMergeTools2(server2, getStateDb) {
20458
20716
  reason: z33.string().describe("Original suggestion reason")
20459
20717
  },
20460
20718
  async ({ source_path, target_path, source_name, target_name, reason }) => {
20461
- const stateDb2 = getStateDb();
20719
+ const stateDb2 = getStateDb2();
20462
20720
  if (!stateDb2) {
20463
20721
  return {
20464
20722
  content: [{ type: "text", text: JSON.stringify({ dismissed: false, error: "StateDb not available" }) }]
@@ -20993,7 +21251,7 @@ function handleTemporalSummary(index, stateDb2, startDate, endDate, focusEntitie
20993
21251
  content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
20994
21252
  };
20995
21253
  }
20996
- function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getStateDb) {
21254
+ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getStateDb2) {
20997
21255
  server2.registerTool(
20998
21256
  "get_context_around_date",
20999
21257
  {
@@ -21008,7 +21266,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
21008
21266
  async ({ date, window_days, limit: requestedLimit }) => {
21009
21267
  requireIndex();
21010
21268
  const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
21011
- return handleGetContextAroundDate(getIndex(), getStateDb(), date, window_days ?? 3, limit);
21269
+ return handleGetContextAroundDate(getIndex(), getStateDb2(), date, window_days ?? 3, limit);
21012
21270
  }
21013
21271
  );
21014
21272
  server2.registerTool(
@@ -21030,7 +21288,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
21030
21288
  const limit = Math.min(requestedLimit ?? 30, MAX_LIMIT);
21031
21289
  return handlePredictStaleNotes(
21032
21290
  getIndex(),
21033
- getStateDb(),
21291
+ getStateDb2(),
21034
21292
  days ?? 30,
21035
21293
  min_importance ?? 0,
21036
21294
  include_recommendations ?? true,
@@ -21055,7 +21313,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
21055
21313
  requireIndex();
21056
21314
  return handleTrackConceptEvolution(
21057
21315
  getIndex(),
21058
- getStateDb(),
21316
+ getStateDb2(),
21059
21317
  entity,
21060
21318
  days_back ?? 90,
21061
21319
  include_cooccurrence ?? true
@@ -21079,7 +21337,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
21079
21337
  const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
21080
21338
  return handleTemporalSummary(
21081
21339
  getIndex(),
21082
- getStateDb(),
21340
+ getStateDb2(),
21083
21341
  start_date,
21084
21342
  end_date,
21085
21343
  focus_entities,
@@ -21094,6 +21352,7 @@ init_serverLog();
21094
21352
  init_wikilinkFeedback();
21095
21353
  init_recency();
21096
21354
  init_cooccurrence();
21355
+ init_retrievalCooccurrence();
21097
21356
  init_corrections();
21098
21357
  init_edgeWeights();
21099
21358
  import * as fs32 from "node:fs/promises";
@@ -21244,10 +21503,14 @@ function parseVaultConfig() {
21244
21503
  for (const entry of envValue.split(",")) {
21245
21504
  const trimmed = entry.trim();
21246
21505
  const colonIdx = trimmed.indexOf(":");
21247
- if (colonIdx <= 0) continue;
21506
+ if (colonIdx <= 0) {
21507
+ console.error(`[flywheel] Warning: skipping malformed FLYWHEEL_VAULTS entry: "${trimmed}" (expected name:/path)`);
21508
+ continue;
21509
+ }
21248
21510
  let name;
21249
21511
  let vaultPath2;
21250
21512
  if (colonIdx === 1 && trimmed.length > 2 && (trimmed[2] === "\\" || trimmed[2] === "/")) {
21513
+ console.error(`[flywheel] Warning: skipping ambiguous FLYWHEEL_VAULTS entry: "${trimmed}" (looks like a Windows path, not name:path)`);
21251
21514
  continue;
21252
21515
  }
21253
21516
  name = trimmed.substring(0, colonIdx);
@@ -21271,6 +21534,11 @@ try {
21271
21534
  } catch {
21272
21535
  resolvedVaultPath = vaultPath.replace(/\\/g, "/");
21273
21536
  }
21537
+ if (!existsSync3(resolvedVaultPath)) {
21538
+ console.error(`[flywheel] Fatal: vault path does not exist: ${resolvedVaultPath}`);
21539
+ console.error(`[flywheel] Set PROJECT_PATH or VAULT_PATH to a valid Obsidian vault directory.`);
21540
+ process.exit(1);
21541
+ }
21274
21542
  var vaultIndex;
21275
21543
  var flywheelConfig = {};
21276
21544
  var stateDb = null;
@@ -21574,7 +21842,7 @@ Use "note_intelligence" for per-note analysis (completeness, quality, suggestion
21574
21842
  }
21575
21843
  return parts.join("\n");
21576
21844
  }
21577
- function applyToolGating(targetServer, categories, getDb, registry) {
21845
+ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPath) {
21578
21846
  let _registered = 0;
21579
21847
  let _skipped = 0;
21580
21848
  function gate(name) {
@@ -21591,6 +21859,7 @@ function applyToolGating(targetServer, categories, getDb, registry) {
21591
21859
  const start = Date.now();
21592
21860
  let success = true;
21593
21861
  let notePaths;
21862
+ let result;
21594
21863
  const params = args[0];
21595
21864
  if (params && typeof params === "object") {
21596
21865
  const paths = [];
@@ -21602,12 +21871,13 @@ function applyToolGating(targetServer, categories, getDb, registry) {
21602
21871
  if (paths.length > 0) notePaths = paths;
21603
21872
  }
21604
21873
  try {
21605
- return await handler(...args);
21874
+ result = await handler(...args);
21875
+ return result;
21606
21876
  } catch (err) {
21607
21877
  success = false;
21608
21878
  throw err;
21609
21879
  } finally {
21610
- const db4 = getDb();
21880
+ const db4 = getDb4();
21611
21881
  if (db4) {
21612
21882
  try {
21613
21883
  let sessionId;
@@ -21615,12 +21885,36 @@ function applyToolGating(targetServer, categories, getDb, registry) {
21615
21885
  sessionId = getSessionId();
21616
21886
  } catch {
21617
21887
  }
21888
+ let responseTokens;
21889
+ if (result?.content) {
21890
+ let totalChars = 0;
21891
+ for (const block of result.content) {
21892
+ if (block?.type === "text" && typeof block.text === "string") {
21893
+ totalChars += block.text.length;
21894
+ }
21895
+ }
21896
+ if (totalChars > 0) responseTokens = Math.ceil(totalChars / 4);
21897
+ }
21898
+ let baselineTokens;
21899
+ if (notePaths && notePaths.length > 0 && getVaultPath) {
21900
+ const vp = getVaultPath();
21901
+ let totalBytes = 0;
21902
+ for (const p of notePaths) {
21903
+ try {
21904
+ totalBytes += statSync5(path32.join(vp, p)).size;
21905
+ } catch {
21906
+ }
21907
+ }
21908
+ if (totalBytes > 0) baselineTokens = Math.ceil(totalBytes / 4);
21909
+ }
21618
21910
  recordToolInvocation(db4, {
21619
21911
  tool_name: toolName,
21620
21912
  session_id: sessionId,
21621
21913
  note_paths: notePaths,
21622
21914
  duration_ms: Date.now() - start,
21623
- success
21915
+ success,
21916
+ response_tokens: responseTokens,
21917
+ baseline_tokens: baselineTokens
21624
21918
  });
21625
21919
  } catch {
21626
21920
  }
@@ -21642,21 +21936,15 @@ function applyToolGating(targetServer, categories, getDb, registry) {
21642
21936
  }
21643
21937
  const ctx = registry.getContext(vaultName);
21644
21938
  activateVault(ctx);
21645
- stateDb = ctx.stateDb;
21646
- vaultIndex = ctx.vaultIndex;
21647
- flywheelConfig = ctx.flywheelConfig;
21648
- return handler(...args);
21939
+ return runInVaultScope(buildVaultScope(ctx), () => handler(...args));
21649
21940
  };
21650
21941
  }
21651
21942
  async function crossVaultSearch(reg, handler, args) {
21652
21943
  const perVault = [];
21653
21944
  for (const ctx of reg.getAllContexts()) {
21654
21945
  activateVault(ctx);
21655
- stateDb = ctx.stateDb;
21656
- vaultIndex = ctx.vaultIndex;
21657
- flywheelConfig = ctx.flywheelConfig;
21658
21946
  try {
21659
- const result = await handler(...args);
21947
+ const result = await runInVaultScope(buildVaultScope(ctx), () => handler(...args));
21660
21948
  const text = result?.content?.[0]?.text;
21661
21949
  if (text) {
21662
21950
  perVault.push({ vault: ctx.name, data: JSON.parse(text) });
@@ -21738,39 +22026,43 @@ function applyToolGating(targetServer, categories, getDb, registry) {
21738
22026
  } };
21739
22027
  }
21740
22028
  function registerAllTools(targetServer) {
21741
- registerHealthTools(targetServer, () => vaultIndex, () => vaultPath, () => flywheelConfig, () => stateDb, getWatcherStatus);
22029
+ const gvp = () => getActiveScopeOrNull()?.vaultPath ?? vaultPath;
22030
+ const gvi = () => getActiveScopeOrNull()?.vaultIndex ?? vaultIndex;
22031
+ const gsd = () => getActiveScopeOrNull()?.stateDb ?? stateDb;
22032
+ const gcf = () => getActiveScopeOrNull()?.flywheelConfig ?? flywheelConfig;
22033
+ registerHealthTools(targetServer, gvi, gvp, gcf, gsd, getWatcherStatus);
21742
22034
  registerSystemTools(
21743
22035
  targetServer,
21744
- () => vaultIndex,
22036
+ gvi,
21745
22037
  (newIndex) => {
21746
22038
  vaultIndex = newIndex;
21747
22039
  },
21748
- () => vaultPath,
22040
+ gvp,
21749
22041
  (newConfig) => {
21750
22042
  flywheelConfig = newConfig;
21751
22043
  setWikilinkConfig(newConfig);
21752
22044
  },
21753
- () => stateDb
22045
+ gsd
21754
22046
  );
21755
- registerGraphTools(targetServer, () => vaultIndex, () => vaultPath, () => stateDb);
21756
- registerWikilinkTools(targetServer, () => vaultIndex, () => vaultPath, () => stateDb);
21757
- registerQueryTools(targetServer, () => vaultIndex, () => vaultPath, () => stateDb);
21758
- registerPrimitiveTools(targetServer, () => vaultIndex, () => vaultPath, () => flywheelConfig, () => stateDb);
21759
- registerGraphAnalysisTools(targetServer, () => vaultIndex, () => vaultPath, () => stateDb, () => flywheelConfig);
21760
- registerSemanticAnalysisTools(targetServer, () => vaultIndex, () => vaultPath);
21761
- registerVaultSchemaTools(targetServer, () => vaultIndex, () => vaultPath);
21762
- registerNoteIntelligenceTools(targetServer, () => vaultIndex, () => vaultPath, () => flywheelConfig);
21763
- registerMigrationTools(targetServer, () => vaultIndex, () => vaultPath);
21764
- registerMutationTools(targetServer, () => vaultPath, () => flywheelConfig);
21765
- registerTaskTools(targetServer, () => vaultPath);
21766
- registerFrontmatterTools(targetServer, () => vaultPath);
21767
- registerNoteTools(targetServer, () => vaultPath, () => vaultIndex);
21768
- registerMoveNoteTools(targetServer, () => vaultPath);
21769
- registerMergeTools(targetServer, () => vaultPath);
21770
- registerSystemTools2(targetServer, () => vaultPath);
21771
- registerPolicyTools(targetServer, () => vaultPath, () => {
21772
- if (!vaultIndex) return void 0;
21773
- const index = vaultIndex;
22047
+ registerGraphTools(targetServer, gvi, gvp, gsd);
22048
+ registerWikilinkTools(targetServer, gvi, gvp, gsd);
22049
+ registerQueryTools(targetServer, gvi, gvp, gsd);
22050
+ registerPrimitiveTools(targetServer, gvi, gvp, gcf, gsd);
22051
+ registerGraphAnalysisTools(targetServer, gvi, gvp, gsd, gcf);
22052
+ registerSemanticAnalysisTools(targetServer, gvi, gvp);
22053
+ registerVaultSchemaTools(targetServer, gvi, gvp);
22054
+ registerNoteIntelligenceTools(targetServer, gvi, gvp, gcf);
22055
+ registerMigrationTools(targetServer, gvi, gvp);
22056
+ registerMutationTools(targetServer, gvp, gcf);
22057
+ registerTaskTools(targetServer, gvp);
22058
+ registerFrontmatterTools(targetServer, gvp);
22059
+ registerNoteTools(targetServer, gvp, gvi);
22060
+ registerMoveNoteTools(targetServer, gvp);
22061
+ registerMergeTools(targetServer, gvp);
22062
+ registerSystemTools2(targetServer, gvp);
22063
+ registerPolicyTools(targetServer, gvp, () => {
22064
+ const index = gvi();
22065
+ if (!index) return void 0;
21774
22066
  return ({ query, folder, where, limit = 10 }) => {
21775
22067
  let notes = Array.from(index.notes.values());
21776
22068
  if (folder) {
@@ -21798,42 +22090,42 @@ function registerAllTools(targetServer) {
21798
22090
  }));
21799
22091
  };
21800
22092
  });
21801
- registerTagTools(targetServer, () => vaultIndex, () => vaultPath);
21802
- registerWikilinkFeedbackTools(targetServer, () => stateDb);
21803
- registerCorrectionTools(targetServer, () => stateDb);
21804
- registerInitTools(targetServer, () => vaultPath, () => stateDb);
22093
+ registerTagTools(targetServer, gvi, gvp);
22094
+ registerWikilinkFeedbackTools(targetServer, gsd);
22095
+ registerCorrectionTools(targetServer, gsd);
22096
+ registerInitTools(targetServer, gvp, gsd);
21805
22097
  registerConfigTools(
21806
22098
  targetServer,
21807
- () => flywheelConfig,
22099
+ gcf,
21808
22100
  (newConfig) => {
21809
22101
  flywheelConfig = newConfig;
21810
22102
  setWikilinkConfig(newConfig);
21811
22103
  },
21812
- () => stateDb
22104
+ gsd
21813
22105
  );
21814
- registerMetricsTools(targetServer, () => vaultIndex, () => stateDb);
21815
- registerActivityTools(targetServer, () => stateDb, () => {
22106
+ registerMetricsTools(targetServer, gvi, gsd);
22107
+ registerActivityTools(targetServer, gsd, () => {
21816
22108
  try {
21817
22109
  return getSessionId();
21818
22110
  } catch {
21819
22111
  return null;
21820
22112
  }
21821
22113
  });
21822
- registerSimilarityTools(targetServer, () => vaultIndex, () => vaultPath, () => stateDb);
21823
- registerSemanticTools(targetServer, () => vaultPath, () => stateDb);
21824
- registerMergeTools2(targetServer, () => stateDb);
21825
- registerTemporalAnalysisTools(targetServer, () => vaultIndex, () => vaultPath, () => stateDb);
21826
- registerMemoryTools(targetServer, () => stateDb);
21827
- registerRecallTools(targetServer, () => stateDb, () => vaultPath, () => vaultIndex ?? null);
21828
- registerBriefTools(targetServer, () => stateDb);
21829
- registerVaultResources(targetServer, () => vaultIndex ?? null);
22114
+ registerSimilarityTools(targetServer, gvi, gvp, gsd);
22115
+ registerSemanticTools(targetServer, gvp, gsd);
22116
+ registerMergeTools2(targetServer, gsd);
22117
+ registerTemporalAnalysisTools(targetServer, gvi, gvp, gsd);
22118
+ registerMemoryTools(targetServer, gsd);
22119
+ registerRecallTools(targetServer, gsd, gvp, () => gvi() ?? null);
22120
+ registerBriefTools(targetServer, gsd);
22121
+ registerVaultResources(targetServer, () => gvi() ?? null);
21830
22122
  }
21831
22123
  function createConfiguredServer() {
21832
22124
  const s = new McpServer(
21833
22125
  { name: "flywheel-memory", version: pkg.version },
21834
22126
  { instructions: generateInstructions(enabledCategories, vaultRegistry) }
21835
22127
  );
21836
- applyToolGating(s, enabledCategories, () => stateDb, vaultRegistry);
22128
+ applyToolGating(s, enabledCategories, () => stateDb, vaultRegistry, () => vaultPath);
21837
22129
  registerAllTools(s);
21838
22130
  return s;
21839
22131
  }
@@ -21841,7 +22133,7 @@ var server = new McpServer(
21841
22133
  { name: "flywheel-memory", version: pkg.version },
21842
22134
  { instructions: generateInstructions(enabledCategories, vaultRegistry) }
21843
22135
  );
21844
- var _gatingResult = applyToolGating(server, enabledCategories, () => stateDb, vaultRegistry);
22136
+ var _gatingResult = applyToolGating(server, enabledCategories, () => stateDb, vaultRegistry, () => vaultPath);
21845
22137
  registerAllTools(server);
21846
22138
  var categoryList = Array.from(enabledCategories).sort().join(", ");
21847
22139
  serverLog("server", `Tool categories: ${categoryList}`);
@@ -21875,6 +22167,20 @@ async function initializeVault(name, vaultPathArg) {
21875
22167
  }
21876
22168
  return ctx;
21877
22169
  }
22170
+ function buildVaultScope(ctx) {
22171
+ return {
22172
+ name: ctx.name,
22173
+ vaultPath: ctx.vaultPath,
22174
+ stateDb: ctx.stateDb,
22175
+ flywheelConfig: ctx.flywheelConfig,
22176
+ vaultIndex: ctx.vaultIndex,
22177
+ cooccurrenceIndex: ctx.cooccurrenceIndex,
22178
+ indexState: ctx.indexState,
22179
+ indexError: ctx.indexError,
22180
+ embeddingsBuilding: ctx.embeddingsBuilding,
22181
+ entityEmbeddingsMap: getEntityEmbeddingsMap()
22182
+ };
22183
+ }
21878
22184
  function activateVault(ctx) {
21879
22185
  globalThis.__flywheel_active_vault = ctx.name;
21880
22186
  if (ctx.stateDb) {
@@ -21891,16 +22197,7 @@ function activateVault(ctx) {
21891
22197
  setIndexState(ctx.indexState);
21892
22198
  setIndexError(ctx.indexError);
21893
22199
  setEmbeddingsBuilding(ctx.embeddingsBuilding);
21894
- setActiveScope({
21895
- name: ctx.name,
21896
- vaultPath: ctx.vaultPath,
21897
- stateDb: ctx.stateDb,
21898
- flywheelConfig: ctx.flywheelConfig,
21899
- cooccurrenceIndex: ctx.cooccurrenceIndex,
21900
- indexState: ctx.indexState,
21901
- indexError: ctx.indexError,
21902
- embeddingsBuilding: ctx.embeddingsBuilding
21903
- });
22200
+ setActiveScope(buildVaultScope(ctx));
21904
22201
  }
21905
22202
  function getActiveVaultContext() {
21906
22203
  if (!vaultRegistry) return null;
@@ -21967,6 +22264,10 @@ async function main() {
21967
22264
  const { createMcpExpressApp } = await import("@modelcontextprotocol/sdk/server/express.js");
21968
22265
  const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
21969
22266
  const httpPort = parseInt(process.env.FLYWHEEL_HTTP_PORT ?? "3111", 10);
22267
+ if (!Number.isFinite(httpPort) || httpPort < 1 || httpPort > 65535) {
22268
+ console.error(`[flywheel] Fatal: invalid FLYWHEEL_HTTP_PORT: ${process.env.FLYWHEEL_HTTP_PORT} (must be 1-65535)`);
22269
+ process.exit(1);
22270
+ }
21970
22271
  const httpHost = process.env.FLYWHEEL_HTTP_HOST ?? "127.0.0.1";
21971
22272
  const app = createMcpExpressApp({ host: httpHost });
21972
22273
  app.post("/mcp", async (req, res) => {
@@ -22139,6 +22440,7 @@ function runPeriodicMaintenance(db4) {
22139
22440
  purgeOldSuggestionEvents(db4, 30);
22140
22441
  purgeOldNoteLinkHistory(db4, 90);
22141
22442
  purgeOldSnapshots(db4, 90);
22443
+ pruneStaleRetrievalCooccurrence(db4, 30);
22142
22444
  lastPurgeAt = now;
22143
22445
  serverLog("server", "Daily purge complete");
22144
22446
  }
@@ -23012,6 +23314,21 @@ async function runPostIndexWork(index) {
23012
23314
  tracker.end({ error: String(e) });
23013
23315
  serverLog("watcher", `Tag scan: failed: ${e}`, "error");
23014
23316
  }
23317
+ tracker.start("retrieval_cooccurrence", {});
23318
+ try {
23319
+ if (stateDb) {
23320
+ const inserted = mineRetrievalCooccurrence(stateDb);
23321
+ tracker.end({ pairs_inserted: inserted });
23322
+ if (inserted > 0) {
23323
+ serverLog("watcher", `Retrieval co-occurrence: ${inserted} new pairs`);
23324
+ }
23325
+ } else {
23326
+ tracker.end({ skipped: "no stateDb" });
23327
+ }
23328
+ } catch (e) {
23329
+ tracker.end({ error: String(e) });
23330
+ serverLog("watcher", `Retrieval co-occurrence: failed: ${e}`, "error");
23331
+ }
23015
23332
  const duration = Date.now() - batchStart;
23016
23333
  if (stateDb) {
23017
23334
  recordIndexEvent(stateDb, {