@velvetmonkey/flywheel-memory 2.0.119 → 2.0.120

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 +529 -213
  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,20 @@ 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
+ const stateDb2 = getStateDb2();
9912
+ if (stateDb2) {
9913
+ try {
9914
+ const result = stateDb2.db.pragma("quick_check");
9915
+ const ok = result.length === 1 && Object.values(result[0])[0] === "ok";
9916
+ if (!ok) {
9917
+ overall = "unhealthy";
9918
+ recommendations.push(`Database integrity check failed: ${Object.values(result[0])[0] ?? "unknown error"}`);
9919
+ }
9920
+ } catch (err) {
9921
+ overall = "unhealthy";
9922
+ recommendations.push(`Database integrity check error: ${err instanceof Error ? err.message : err}`);
9923
+ }
9924
+ }
9724
9925
  const indexBuilt = indexState2 === "ready" && index !== void 0 && index.notes !== void 0;
9725
9926
  const indexAge = indexBuilt && index.builtAt ? Math.floor((Date.now() - index.builtAt.getTime()) / 1e3) : -1;
9726
9927
  const indexStale = indexBuilt && indexAge > STALE_THRESHOLD_SECONDS;
@@ -9769,7 +9970,6 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
9769
9970
  const config = getConfig2();
9770
9971
  const configInfo = Object.keys(config).length > 0 ? config : void 0;
9771
9972
  let lastRebuild;
9772
- const stateDb2 = getStateDb();
9773
9973
  if (stateDb2) {
9774
9974
  try {
9775
9975
  const events = getRecentIndexEvents(stateDb2, 1);
@@ -10071,7 +10271,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
10071
10271
  const checks = [];
10072
10272
  const index = getIndex();
10073
10273
  const vaultPath2 = getVaultPath();
10074
- const stateDb2 = getStateDb();
10274
+ const stateDb2 = getStateDb2();
10075
10275
  const watcherStatus = getWatcherStatus2();
10076
10276
  checks.push({
10077
10277
  name: "schema_version",
@@ -10517,6 +10717,7 @@ function enrichNoteResult(notePath, stateDb2, index) {
10517
10717
  }
10518
10718
 
10519
10719
  // src/tools/read/query.ts
10720
+ init_wikilinkFeedback();
10520
10721
  function matchesFrontmatter(note, where) {
10521
10722
  for (const [key, value] of Object.entries(where)) {
10522
10723
  const noteValue = note.frontmatter[key];
@@ -10584,7 +10785,7 @@ function sortNotes(notes, sortBy, order) {
10584
10785
  });
10585
10786
  return sorted;
10586
10787
  }
10587
- function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10788
+ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb2) {
10588
10789
  server2.tool(
10589
10790
  "search",
10590
10791
  '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 +10819,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10618
10819
  const index = getIndex();
10619
10820
  const vaultPath2 = getVaultPath();
10620
10821
  if (prefix && query) {
10621
- const stateDb2 = getStateDb();
10822
+ const stateDb2 = getStateDb2();
10622
10823
  if (!stateDb2) {
10623
10824
  return { content: [{ type: "text", text: JSON.stringify({ results: [], count: 0, query, error: "StateDb not initialized" }, null, 2) }] };
10624
10825
  }
@@ -10666,7 +10867,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10666
10867
  matchingNotes = sortNotes(matchingNotes, sort_by ?? "modified", order ?? "desc");
10667
10868
  const totalMatches = matchingNotes.length;
10668
10869
  const limitedNotes = matchingNotes.slice(0, limit);
10669
- const stateDb2 = getStateDb();
10870
+ const stateDb2 = getStateDb2();
10670
10871
  const notes = limitedNotes.map(
10671
10872
  (note, i) => (i < detailN ? enrichResult : enrichResultLight)({ path: note.path, title: note.title }, index, stateDb2)
10672
10873
  );
@@ -10694,7 +10895,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10694
10895
  }
10695
10896
  const fts5Results = searchFTS5(vaultPath2, query, limit);
10696
10897
  let entityResults = [];
10697
- const stateDbEntity = getStateDb();
10898
+ const stateDbEntity = getStateDb2();
10698
10899
  if (stateDbEntity) {
10699
10900
  try {
10700
10901
  entityResults = searchEntities(stateDbEntity, query, limit);
@@ -10703,7 +10904,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10703
10904
  }
10704
10905
  let edgeRanked = [];
10705
10906
  if (context_note) {
10706
- const ctxStateDb = getStateDb();
10907
+ const ctxStateDb = getStateDb2();
10707
10908
  if (ctxStateDb) {
10708
10909
  try {
10709
10910
  const edgeRows = ctxStateDb.db.prepare(`
@@ -10768,7 +10969,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10768
10969
  }));
10769
10970
  scored.sort((a, b) => b.rrf_score - a.rrf_score);
10770
10971
  const filtered = applyFolderFilter(scored);
10771
- const stateDb2 = getStateDb();
10972
+ const stateDb2 = getStateDb2();
10772
10973
  const results = filtered.slice(0, limit).map((item, i) => ({
10773
10974
  ...(i < detailN ? enrichResult : enrichResultLight)({ path: item.path, title: item.title, snippet: item.snippet }, index, stateDb2),
10774
10975
  rrf_score: item.rrf_score,
@@ -10776,6 +10977,34 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10776
10977
  in_semantic: item.in_semantic,
10777
10978
  in_entity: item.in_entity
10778
10979
  }));
10980
+ if (stateDb2 && results.length < limit) {
10981
+ const existingPaths = new Set(results.map((r) => r.path));
10982
+ const backfill = [];
10983
+ for (const r of results.slice(0, 3)) {
10984
+ const rPath = r.path;
10985
+ if (!rPath) continue;
10986
+ try {
10987
+ const outlinks = getStoredNoteLinks(stateDb2, rPath);
10988
+ for (const target of outlinks) {
10989
+ const entityRow = stateDb2.db.prepare(
10990
+ "SELECT path FROM entities WHERE name_lower = ?"
10991
+ ).get(target);
10992
+ if (entityRow?.path && !existingPaths.has(entityRow.path)) {
10993
+ existingPaths.add(entityRow.path);
10994
+ backfill.push({
10995
+ ...enrichResultLight({ path: entityRow.path, title: target }, index, stateDb2),
10996
+ rrf_score: 0,
10997
+ in_fts5: false,
10998
+ in_semantic: false,
10999
+ in_entity: false
11000
+ });
11001
+ }
11002
+ }
11003
+ } catch {
11004
+ }
11005
+ }
11006
+ results.push(...backfill.slice(0, limit - results.length));
11007
+ }
10779
11008
  return { content: [{ type: "text", text: JSON.stringify({
10780
11009
  method: "hybrid",
10781
11010
  query,
@@ -10795,12 +11024,36 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10795
11024
  ...entityRanked.map((r) => ({ path: r.path, title: r.name, snippet: void 0, in_entity: true }))
10796
11025
  ];
10797
11026
  const filtered = applyFolderFilter(mergedItems);
10798
- const stateDb2 = getStateDb();
11027
+ const stateDb2 = getStateDb2();
10799
11028
  const sliced = filtered.slice(0, limit);
10800
11029
  const results = sliced.map((item, i) => ({
10801
11030
  ...(i < detailN ? enrichResult : enrichResultLight)({ path: item.path, title: item.title, snippet: item.snippet }, index, stateDb2),
10802
11031
  ..."in_fts5" in item ? { in_fts5: true } : { in_entity: true }
10803
11032
  }));
11033
+ if (stateDb2 && results.length < limit) {
11034
+ const existingPaths = new Set(results.map((r) => r.path));
11035
+ const backfill = [];
11036
+ for (const r of results.slice(0, 3)) {
11037
+ const rPath = r.path;
11038
+ if (!rPath) continue;
11039
+ try {
11040
+ const outlinks = getStoredNoteLinks(stateDb2, rPath);
11041
+ for (const target of outlinks) {
11042
+ const entityRow = stateDb2.db.prepare(
11043
+ "SELECT path FROM entities WHERE name_lower = ?"
11044
+ ).get(target);
11045
+ if (entityRow?.path && !existingPaths.has(entityRow.path)) {
11046
+ existingPaths.add(entityRow.path);
11047
+ backfill.push({
11048
+ ...enrichResultLight({ path: entityRow.path, title: target }, index, stateDb2)
11049
+ });
11050
+ }
11051
+ }
11052
+ } catch {
11053
+ }
11054
+ }
11055
+ results.push(...backfill.slice(0, limit - results.length));
11056
+ }
10804
11057
  return { content: [{ type: "text", text: JSON.stringify({
10805
11058
  method: "fts5",
10806
11059
  query,
@@ -10808,7 +11061,7 @@ function registerQueryTools(server2, getIndex, getVaultPath, getStateDb) {
10808
11061
  results
10809
11062
  }, null, 2) }] };
10810
11063
  }
10811
- const stateDbFts = getStateDb();
11064
+ const stateDbFts = getStateDb2();
10812
11065
  const fts5Filtered = applyFolderFilter(fts5Results);
10813
11066
  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
11067
  return { content: [{ type: "text", text: JSON.stringify({
@@ -10896,7 +11149,7 @@ function suggestEntityAliases(stateDb2, folder) {
10896
11149
 
10897
11150
  // src/tools/read/system.ts
10898
11151
  init_edgeWeights();
10899
- function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb) {
11152
+ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfig, getStateDb2) {
10900
11153
  const RefreshIndexOutputSchema = {
10901
11154
  success: z5.boolean().describe("Whether the refresh succeeded"),
10902
11155
  notes_count: z5.number().describe("Number of notes indexed"),
@@ -10922,7 +11175,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
10922
11175
  const newIndex = await buildVaultIndex(vaultPath2);
10923
11176
  setIndex(newIndex);
10924
11177
  setIndexState("ready");
10925
- const stateDb2 = getStateDb?.();
11178
+ const stateDb2 = getStateDb2?.();
10926
11179
  if (stateDb2) {
10927
11180
  try {
10928
11181
  const entityIndex2 = await scanVaultEntities2(vaultPath2, {
@@ -11009,7 +11262,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
11009
11262
  setIndexState("error");
11010
11263
  setIndexError(err instanceof Error ? err : new Error(String(err)));
11011
11264
  const duration = Date.now() - startTime;
11012
- const stateDb2 = getStateDb?.();
11265
+ const stateDb2 = getStateDb2?.();
11013
11266
  if (stateDb2) {
11014
11267
  recordIndexEvent(stateDb2, {
11015
11268
  trigger: "manual_refresh",
@@ -11267,7 +11520,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
11267
11520
  category,
11268
11521
  limit: perCategoryLimit
11269
11522
  }) => {
11270
- const stateDb2 = getStateDb?.();
11523
+ const stateDb2 = getStateDb2?.();
11271
11524
  if (!stateDb2) {
11272
11525
  return {
11273
11526
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
@@ -11322,7 +11575,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
11322
11575
  folder,
11323
11576
  limit: requestedLimit
11324
11577
  }) => {
11325
- const stateDb2 = getStateDb?.();
11578
+ const stateDb2 = getStateDb2?.();
11326
11579
  if (!stateDb2) {
11327
11580
  return {
11328
11581
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
@@ -11542,7 +11795,7 @@ async function findSections(index, headingPattern, vaultPath2, folder) {
11542
11795
 
11543
11796
  // src/tools/read/primitives.ts
11544
11797
  import { getEntityByName as getEntityByName3 } from "@velvetmonkey/vault-core";
11545
- function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb = () => null) {
11798
+ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = () => ({}), getStateDb2 = () => null) {
11546
11799
  server2.registerTool(
11547
11800
  "get_note_structure",
11548
11801
  {
@@ -11581,7 +11834,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
11581
11834
  enriched.backlink_count = backlinks.length;
11582
11835
  enriched.outlink_count = note.outlinks.length;
11583
11836
  }
11584
- const stateDb2 = getStateDb();
11837
+ const stateDb2 = getStateDb2();
11585
11838
  if (stateDb2 && note) {
11586
11839
  try {
11587
11840
  const entity = getEntityByName3(stateDb2, note.title);
@@ -12640,7 +12893,7 @@ function getExcludedPaths(index, config) {
12640
12893
  }
12641
12894
  return excluded;
12642
12895
  }
12643
- function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb, getConfig2) {
12896
+ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb2, getConfig2) {
12644
12897
  server2.registerTool(
12645
12898
  "graph_analysis",
12646
12899
  {
@@ -12810,7 +13063,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb,
12810
13063
  };
12811
13064
  }
12812
13065
  case "emerging_hubs": {
12813
- const db4 = getStateDb?.();
13066
+ const db4 = getStateDb2?.();
12814
13067
  if (!db4) {
12815
13068
  return {
12816
13069
  content: [{ type: "text", text: JSON.stringify({
@@ -17558,7 +17811,7 @@ function registerTagTools(server2, getIndex, getVaultPath) {
17558
17811
  // src/tools/write/wikilinkFeedback.ts
17559
17812
  init_wikilinkFeedback();
17560
17813
  import { z as z22 } from "zod";
17561
- function registerWikilinkFeedbackTools(server2, getStateDb) {
17814
+ function registerWikilinkFeedbackTools(server2, getStateDb2) {
17562
17815
  server2.registerTool(
17563
17816
  "wikilink_feedback",
17564
17817
  {
@@ -17579,7 +17832,7 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
17579
17832
  }
17580
17833
  },
17581
17834
  async ({ mode, entity, note_path, context, correct, limit, days_back, granularity, timestamp_before, timestamp_after, skip_status_update }) => {
17582
- const stateDb2 = getStateDb();
17835
+ const stateDb2 = getStateDb2();
17583
17836
  if (!stateDb2) {
17584
17837
  return {
17585
17838
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available \u2014 database not initialized yet" }) }],
@@ -17741,7 +17994,7 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
17741
17994
  // src/tools/write/corrections.ts
17742
17995
  init_corrections();
17743
17996
  import { z as z23 } from "zod";
17744
- function registerCorrectionTools(server2, getStateDb) {
17997
+ function registerCorrectionTools(server2, getStateDb2) {
17745
17998
  server2.tool(
17746
17999
  "vault_record_correction",
17747
18000
  'Record a persistent correction (e.g., "that link is wrong", "undo that"). Survives across sessions.',
@@ -17752,7 +18005,7 @@ function registerCorrectionTools(server2, getStateDb) {
17752
18005
  note_path: z23.string().optional().describe("Note path (if correction is about a specific note)")
17753
18006
  },
17754
18007
  async ({ correction_type, description, entity, note_path }) => {
17755
- const stateDb2 = getStateDb();
18008
+ const stateDb2 = getStateDb2();
17756
18009
  if (!stateDb2) {
17757
18010
  return {
17758
18011
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
@@ -17780,7 +18033,7 @@ function registerCorrectionTools(server2, getStateDb) {
17780
18033
  limit: z23.number().min(1).max(200).default(50).describe("Max entries to return")
17781
18034
  },
17782
18035
  async ({ status, entity, limit }) => {
17783
- const stateDb2 = getStateDb();
18036
+ const stateDb2 = getStateDb2();
17784
18037
  if (!stateDb2) {
17785
18038
  return {
17786
18039
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
@@ -17807,7 +18060,7 @@ function registerCorrectionTools(server2, getStateDb) {
17807
18060
  status: z23.enum(["applied", "dismissed"]).describe("New status")
17808
18061
  },
17809
18062
  async ({ correction_id, status }) => {
17810
- const stateDb2 = getStateDb();
18063
+ const stateDb2 = getStateDb2();
17811
18064
  if (!stateDb2) {
17812
18065
  return {
17813
18066
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
@@ -18155,7 +18408,7 @@ function findContradictions2(stateDb2, entity) {
18155
18408
  }
18156
18409
 
18157
18410
  // src/tools/write/memory.ts
18158
- function registerMemoryTools(server2, getStateDb) {
18411
+ function registerMemoryTools(server2, getStateDb2) {
18159
18412
  server2.tool(
18160
18413
  "memory",
18161
18414
  "Store, retrieve, search, and manage agent working memory. Actions: store, get, search, list, forget, summarize_session.",
@@ -18180,7 +18433,7 @@ function registerMemoryTools(server2, getStateDb) {
18180
18433
  tool_count: z24.number().optional().describe("Number of tool calls in session")
18181
18434
  },
18182
18435
  async (args) => {
18183
- const stateDb2 = getStateDb();
18436
+ const stateDb2 = getStateDb2();
18184
18437
  if (!stateDb2) {
18185
18438
  return {
18186
18439
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
@@ -18771,7 +19024,7 @@ async function performRecall(stateDb2, query, options = {}) {
18771
19024
  }
18772
19025
  return selected;
18773
19026
  }
18774
- function registerRecallTools(server2, getStateDb, getVaultPath, getIndex) {
19027
+ function registerRecallTools(server2, getStateDb2, getVaultPath, getIndex) {
18775
19028
  server2.tool(
18776
19029
  "recall",
18777
19030
  "Query everything the system knows about a topic. Searches across entities, notes, and memories with graph-boosted ranking.",
@@ -18784,7 +19037,7 @@ function registerRecallTools(server2, getStateDb, getVaultPath, getIndex) {
18784
19037
  diversity: z25.number().min(0).max(1).optional().describe("Relevance vs diversity tradeoff (0=max diversity, 1=pure relevance, default: 0.7)")
18785
19038
  },
18786
19039
  async (args) => {
18787
- const stateDb2 = getStateDb();
19040
+ const stateDb2 = getStateDb2();
18788
19041
  if (!stateDb2) {
18789
19042
  return {
18790
19043
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
@@ -18840,15 +19093,17 @@ import { z as z26 } from "zod";
18840
19093
  // src/core/shared/toolTracking.ts
18841
19094
  function recordToolInvocation(stateDb2, event) {
18842
19095
  stateDb2.db.prepare(
18843
- `INSERT INTO tool_invocations (timestamp, tool_name, session_id, note_paths, duration_ms, success)
18844
- VALUES (?, ?, ?, ?, ?, ?)`
19096
+ `INSERT INTO tool_invocations (timestamp, tool_name, session_id, note_paths, duration_ms, success, response_tokens, baseline_tokens)
19097
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
18845
19098
  ).run(
18846
19099
  Date.now(),
18847
19100
  event.tool_name,
18848
19101
  event.session_id ?? null,
18849
19102
  event.note_paths ? JSON.stringify(event.note_paths) : null,
18850
19103
  event.duration_ms ?? null,
18851
- event.success !== false ? 1 : 0
19104
+ event.success !== false ? 1 : 0,
19105
+ event.response_tokens ?? null,
19106
+ event.baseline_tokens ?? null
18852
19107
  );
18853
19108
  }
18854
19109
  function rowToInvocation(row) {
@@ -18859,7 +19114,9 @@ function rowToInvocation(row) {
18859
19114
  session_id: row.session_id,
18860
19115
  note_paths: row.note_paths ? JSON.parse(row.note_paths) : null,
18861
19116
  duration_ms: row.duration_ms,
18862
- success: row.success === 1
19117
+ success: row.success === 1,
19118
+ response_tokens: row.response_tokens,
19119
+ baseline_tokens: row.baseline_tokens
18863
19120
  };
18864
19121
  }
18865
19122
  function getToolUsageSummary(stateDb2, daysBack = 30) {
@@ -19119,7 +19376,7 @@ function buildVaultPulseSection(stateDb2) {
19119
19376
  estimated_tokens: estimateTokens2(content)
19120
19377
  };
19121
19378
  }
19122
- function registerBriefTools(server2, getStateDb) {
19379
+ function registerBriefTools(server2, getStateDb2) {
19123
19380
  server2.tool(
19124
19381
  "brief",
19125
19382
  "Get a startup context briefing: recent sessions, active entities, memories, pending corrections, and vault stats. Call at conversation start.",
@@ -19128,7 +19385,7 @@ function registerBriefTools(server2, getStateDb) {
19128
19385
  focus: z26.string().optional().describe("Focus entity or topic (filters content)")
19129
19386
  },
19130
19387
  async (args) => {
19131
- const stateDb2 = getStateDb();
19388
+ const stateDb2 = getStateDb2();
19132
19389
  if (!stateDb2) {
19133
19390
  return {
19134
19391
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }],
@@ -19189,7 +19446,7 @@ var VALID_CONFIG_KEYS = {
19189
19446
  implicit_patterns: z27.array(z27.string()),
19190
19447
  adaptive_strictness: z27.boolean()
19191
19448
  };
19192
- function registerConfigTools(server2, getConfig2, setConfig, getStateDb) {
19449
+ function registerConfigTools(server2, getConfig2, setConfig, getStateDb2) {
19193
19450
  server2.registerTool(
19194
19451
  "flywheel_config",
19195
19452
  {
@@ -19231,7 +19488,7 @@ function registerConfigTools(server2, getConfig2, setConfig, getStateDb) {
19231
19488
  }) }]
19232
19489
  };
19233
19490
  }
19234
- const stateDb2 = getStateDb();
19491
+ const stateDb2 = getStateDb2();
19235
19492
  if (!stateDb2) {
19236
19493
  return {
19237
19494
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
@@ -19553,7 +19810,7 @@ async function executeEnrich(stateDb2, vaultPath2, dryRun, batchSize, offset) {
19553
19810
  total_enriched: currentTotal
19554
19811
  };
19555
19812
  }
19556
- function registerInitTools(server2, getVaultPath, getStateDb) {
19813
+ function registerInitTools(server2, getVaultPath, getStateDb2) {
19557
19814
  server2.tool(
19558
19815
  "vault_init",
19559
19816
  `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 +19821,7 @@ function registerInitTools(server2, getVaultPath, getStateDb) {
19564
19821
  offset: z28.number().default(0).describe("For enrich mode: skip this many eligible notes (for pagination)")
19565
19822
  },
19566
19823
  async ({ mode, dry_run, batch_size, offset }) => {
19567
- const stateDb2 = getStateDb();
19824
+ const stateDb2 = getStateDb2();
19568
19825
  const vaultPath2 = getVaultPath();
19569
19826
  switch (mode) {
19570
19827
  case "status": {
@@ -19748,7 +20005,7 @@ function purgeOldMetrics(stateDb2, retentionDays = 90) {
19748
20005
  }
19749
20006
 
19750
20007
  // src/tools/read/metrics.ts
19751
- function registerMetricsTools(server2, getIndex, getStateDb) {
20008
+ function registerMetricsTools(server2, getIndex, getStateDb2) {
19752
20009
  server2.registerTool(
19753
20010
  "vault_growth",
19754
20011
  {
@@ -19763,7 +20020,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
19763
20020
  },
19764
20021
  async ({ mode, metric, days_back, limit: eventLimit }) => {
19765
20022
  const index = getIndex();
19766
- const stateDb2 = getStateDb();
20023
+ const stateDb2 = getStateDb2();
19767
20024
  const daysBack = days_back ?? 30;
19768
20025
  let result;
19769
20026
  switch (mode) {
@@ -19832,7 +20089,7 @@ function registerMetricsTools(server2, getIndex, getStateDb) {
19832
20089
 
19833
20090
  // src/tools/read/activity.ts
19834
20091
  import { z as z30 } from "zod";
19835
- function registerActivityTools(server2, getStateDb, getSessionId2) {
20092
+ function registerActivityTools(server2, getStateDb2, getSessionId2) {
19836
20093
  server2.registerTool(
19837
20094
  "vault_activity",
19838
20095
  {
@@ -19846,7 +20103,7 @@ function registerActivityTools(server2, getStateDb, getSessionId2) {
19846
20103
  }
19847
20104
  },
19848
20105
  async ({ mode, session_id, days_back, limit: resultLimit }) => {
19849
- const stateDb2 = getStateDb();
20106
+ const stateDb2 = getStateDb2();
19850
20107
  if (!stateDb2) {
19851
20108
  return {
19852
20109
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
@@ -20181,7 +20438,7 @@ async function findHybridSimilarNotes(db4, vaultPath2, index, sourcePath, option
20181
20438
 
20182
20439
  // src/tools/read/similarity.ts
20183
20440
  init_embeddings();
20184
- function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
20441
+ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb2) {
20185
20442
  server2.registerTool(
20186
20443
  "find_similar",
20187
20444
  {
@@ -20196,7 +20453,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
20196
20453
  async ({ path: path33, limit, diversity }) => {
20197
20454
  const index = getIndex();
20198
20455
  const vaultPath2 = getVaultPath();
20199
- const stateDb2 = getStateDb();
20456
+ const stateDb2 = getStateDb2();
20200
20457
  if (!stateDb2) {
20201
20458
  return {
20202
20459
  content: [{ type: "text", text: JSON.stringify({ error: "StateDb not available" }) }]
@@ -20237,7 +20494,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb) {
20237
20494
  init_embeddings();
20238
20495
  import { z as z32 } from "zod";
20239
20496
  import { getAllEntitiesFromDb } from "@velvetmonkey/vault-core";
20240
- function registerSemanticTools(server2, getVaultPath, getStateDb) {
20497
+ function registerSemanticTools(server2, getVaultPath, getStateDb2) {
20241
20498
  server2.registerTool(
20242
20499
  "init_semantic",
20243
20500
  {
@@ -20250,7 +20507,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb) {
20250
20507
  }
20251
20508
  },
20252
20509
  async ({ force }) => {
20253
- const stateDb2 = getStateDb();
20510
+ const stateDb2 = getStateDb2();
20254
20511
  if (!stateDb2) {
20255
20512
  return {
20256
20513
  content: [{
@@ -20344,7 +20601,7 @@ import { getAllEntitiesFromDb as getAllEntitiesFromDb2, getDismissedMergePairs,
20344
20601
  function normalizeName(name) {
20345
20602
  return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
20346
20603
  }
20347
- function registerMergeTools2(server2, getStateDb) {
20604
+ function registerMergeTools2(server2, getStateDb2) {
20348
20605
  server2.tool(
20349
20606
  "suggest_entity_merges",
20350
20607
  "Find potential duplicate entities that could be merged based on name similarity",
@@ -20352,7 +20609,7 @@ function registerMergeTools2(server2, getStateDb) {
20352
20609
  limit: z33.number().optional().default(50).describe("Maximum number of suggestions to return")
20353
20610
  },
20354
20611
  async ({ limit }) => {
20355
- const stateDb2 = getStateDb();
20612
+ const stateDb2 = getStateDb2();
20356
20613
  if (!stateDb2) {
20357
20614
  return {
20358
20615
  content: [{ type: "text", text: JSON.stringify({ suggestions: [], error: "StateDb not available" }) }]
@@ -20458,7 +20715,7 @@ function registerMergeTools2(server2, getStateDb) {
20458
20715
  reason: z33.string().describe("Original suggestion reason")
20459
20716
  },
20460
20717
  async ({ source_path, target_path, source_name, target_name, reason }) => {
20461
- const stateDb2 = getStateDb();
20718
+ const stateDb2 = getStateDb2();
20462
20719
  if (!stateDb2) {
20463
20720
  return {
20464
20721
  content: [{ type: "text", text: JSON.stringify({ dismissed: false, error: "StateDb not available" }) }]
@@ -20993,7 +21250,7 @@ function handleTemporalSummary(index, stateDb2, startDate, endDate, focusEntitie
20993
21250
  content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
20994
21251
  };
20995
21252
  }
20996
- function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getStateDb) {
21253
+ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getStateDb2) {
20997
21254
  server2.registerTool(
20998
21255
  "get_context_around_date",
20999
21256
  {
@@ -21008,7 +21265,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
21008
21265
  async ({ date, window_days, limit: requestedLimit }) => {
21009
21266
  requireIndex();
21010
21267
  const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
21011
- return handleGetContextAroundDate(getIndex(), getStateDb(), date, window_days ?? 3, limit);
21268
+ return handleGetContextAroundDate(getIndex(), getStateDb2(), date, window_days ?? 3, limit);
21012
21269
  }
21013
21270
  );
21014
21271
  server2.registerTool(
@@ -21030,7 +21287,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
21030
21287
  const limit = Math.min(requestedLimit ?? 30, MAX_LIMIT);
21031
21288
  return handlePredictStaleNotes(
21032
21289
  getIndex(),
21033
- getStateDb(),
21290
+ getStateDb2(),
21034
21291
  days ?? 30,
21035
21292
  min_importance ?? 0,
21036
21293
  include_recommendations ?? true,
@@ -21055,7 +21312,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
21055
21312
  requireIndex();
21056
21313
  return handleTrackConceptEvolution(
21057
21314
  getIndex(),
21058
- getStateDb(),
21315
+ getStateDb2(),
21059
21316
  entity,
21060
21317
  days_back ?? 90,
21061
21318
  include_cooccurrence ?? true
@@ -21079,7 +21336,7 @@ function registerTemporalAnalysisTools(server2, getIndex, getVaultPath, getState
21079
21336
  const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
21080
21337
  return handleTemporalSummary(
21081
21338
  getIndex(),
21082
- getStateDb(),
21339
+ getStateDb2(),
21083
21340
  start_date,
21084
21341
  end_date,
21085
21342
  focus_entities,
@@ -21094,6 +21351,7 @@ init_serverLog();
21094
21351
  init_wikilinkFeedback();
21095
21352
  init_recency();
21096
21353
  init_cooccurrence();
21354
+ init_retrievalCooccurrence();
21097
21355
  init_corrections();
21098
21356
  init_edgeWeights();
21099
21357
  import * as fs32 from "node:fs/promises";
@@ -21244,10 +21502,14 @@ function parseVaultConfig() {
21244
21502
  for (const entry of envValue.split(",")) {
21245
21503
  const trimmed = entry.trim();
21246
21504
  const colonIdx = trimmed.indexOf(":");
21247
- if (colonIdx <= 0) continue;
21505
+ if (colonIdx <= 0) {
21506
+ console.error(`[flywheel] Warning: skipping malformed FLYWHEEL_VAULTS entry: "${trimmed}" (expected name:/path)`);
21507
+ continue;
21508
+ }
21248
21509
  let name;
21249
21510
  let vaultPath2;
21250
21511
  if (colonIdx === 1 && trimmed.length > 2 && (trimmed[2] === "\\" || trimmed[2] === "/")) {
21512
+ console.error(`[flywheel] Warning: skipping ambiguous FLYWHEEL_VAULTS entry: "${trimmed}" (looks like a Windows path, not name:path)`);
21251
21513
  continue;
21252
21514
  }
21253
21515
  name = trimmed.substring(0, colonIdx);
@@ -21271,6 +21533,11 @@ try {
21271
21533
  } catch {
21272
21534
  resolvedVaultPath = vaultPath.replace(/\\/g, "/");
21273
21535
  }
21536
+ if (!existsSync3(resolvedVaultPath)) {
21537
+ console.error(`[flywheel] Fatal: vault path does not exist: ${resolvedVaultPath}`);
21538
+ console.error(`[flywheel] Set PROJECT_PATH or VAULT_PATH to a valid Obsidian vault directory.`);
21539
+ process.exit(1);
21540
+ }
21274
21541
  var vaultIndex;
21275
21542
  var flywheelConfig = {};
21276
21543
  var stateDb = null;
@@ -21574,7 +21841,7 @@ Use "note_intelligence" for per-note analysis (completeness, quality, suggestion
21574
21841
  }
21575
21842
  return parts.join("\n");
21576
21843
  }
21577
- function applyToolGating(targetServer, categories, getDb, registry) {
21844
+ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPath) {
21578
21845
  let _registered = 0;
21579
21846
  let _skipped = 0;
21580
21847
  function gate(name) {
@@ -21591,6 +21858,7 @@ function applyToolGating(targetServer, categories, getDb, registry) {
21591
21858
  const start = Date.now();
21592
21859
  let success = true;
21593
21860
  let notePaths;
21861
+ let result;
21594
21862
  const params = args[0];
21595
21863
  if (params && typeof params === "object") {
21596
21864
  const paths = [];
@@ -21602,12 +21870,13 @@ function applyToolGating(targetServer, categories, getDb, registry) {
21602
21870
  if (paths.length > 0) notePaths = paths;
21603
21871
  }
21604
21872
  try {
21605
- return await handler(...args);
21873
+ result = await handler(...args);
21874
+ return result;
21606
21875
  } catch (err) {
21607
21876
  success = false;
21608
21877
  throw err;
21609
21878
  } finally {
21610
- const db4 = getDb();
21879
+ const db4 = getDb4();
21611
21880
  if (db4) {
21612
21881
  try {
21613
21882
  let sessionId;
@@ -21615,12 +21884,36 @@ function applyToolGating(targetServer, categories, getDb, registry) {
21615
21884
  sessionId = getSessionId();
21616
21885
  } catch {
21617
21886
  }
21887
+ let responseTokens;
21888
+ if (result?.content) {
21889
+ let totalChars = 0;
21890
+ for (const block of result.content) {
21891
+ if (block?.type === "text" && typeof block.text === "string") {
21892
+ totalChars += block.text.length;
21893
+ }
21894
+ }
21895
+ if (totalChars > 0) responseTokens = Math.ceil(totalChars / 4);
21896
+ }
21897
+ let baselineTokens;
21898
+ if (notePaths && notePaths.length > 0 && getVaultPath) {
21899
+ const vp = getVaultPath();
21900
+ let totalBytes = 0;
21901
+ for (const p of notePaths) {
21902
+ try {
21903
+ totalBytes += statSync5(path32.join(vp, p)).size;
21904
+ } catch {
21905
+ }
21906
+ }
21907
+ if (totalBytes > 0) baselineTokens = Math.ceil(totalBytes / 4);
21908
+ }
21618
21909
  recordToolInvocation(db4, {
21619
21910
  tool_name: toolName,
21620
21911
  session_id: sessionId,
21621
21912
  note_paths: notePaths,
21622
21913
  duration_ms: Date.now() - start,
21623
- success
21914
+ success,
21915
+ response_tokens: responseTokens,
21916
+ baseline_tokens: baselineTokens
21624
21917
  });
21625
21918
  } catch {
21626
21919
  }
@@ -21642,21 +21935,15 @@ function applyToolGating(targetServer, categories, getDb, registry) {
21642
21935
  }
21643
21936
  const ctx = registry.getContext(vaultName);
21644
21937
  activateVault(ctx);
21645
- stateDb = ctx.stateDb;
21646
- vaultIndex = ctx.vaultIndex;
21647
- flywheelConfig = ctx.flywheelConfig;
21648
- return handler(...args);
21938
+ return runInVaultScope(buildVaultScope(ctx), () => handler(...args));
21649
21939
  };
21650
21940
  }
21651
21941
  async function crossVaultSearch(reg, handler, args) {
21652
21942
  const perVault = [];
21653
21943
  for (const ctx of reg.getAllContexts()) {
21654
21944
  activateVault(ctx);
21655
- stateDb = ctx.stateDb;
21656
- vaultIndex = ctx.vaultIndex;
21657
- flywheelConfig = ctx.flywheelConfig;
21658
21945
  try {
21659
- const result = await handler(...args);
21946
+ const result = await runInVaultScope(buildVaultScope(ctx), () => handler(...args));
21660
21947
  const text = result?.content?.[0]?.text;
21661
21948
  if (text) {
21662
21949
  perVault.push({ vault: ctx.name, data: JSON.parse(text) });
@@ -21738,39 +22025,43 @@ function applyToolGating(targetServer, categories, getDb, registry) {
21738
22025
  } };
21739
22026
  }
21740
22027
  function registerAllTools(targetServer) {
21741
- registerHealthTools(targetServer, () => vaultIndex, () => vaultPath, () => flywheelConfig, () => stateDb, getWatcherStatus);
22028
+ const gvp = () => getActiveScopeOrNull()?.vaultPath ?? vaultPath;
22029
+ const gvi = () => getActiveScopeOrNull()?.vaultIndex ?? vaultIndex;
22030
+ const gsd = () => getActiveScopeOrNull()?.stateDb ?? stateDb;
22031
+ const gcf = () => getActiveScopeOrNull()?.flywheelConfig ?? flywheelConfig;
22032
+ registerHealthTools(targetServer, gvi, gvp, gcf, gsd, getWatcherStatus);
21742
22033
  registerSystemTools(
21743
22034
  targetServer,
21744
- () => vaultIndex,
22035
+ gvi,
21745
22036
  (newIndex) => {
21746
22037
  vaultIndex = newIndex;
21747
22038
  },
21748
- () => vaultPath,
22039
+ gvp,
21749
22040
  (newConfig) => {
21750
22041
  flywheelConfig = newConfig;
21751
22042
  setWikilinkConfig(newConfig);
21752
22043
  },
21753
- () => stateDb
22044
+ gsd
21754
22045
  );
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;
22046
+ registerGraphTools(targetServer, gvi, gvp, gsd);
22047
+ registerWikilinkTools(targetServer, gvi, gvp, gsd);
22048
+ registerQueryTools(targetServer, gvi, gvp, gsd);
22049
+ registerPrimitiveTools(targetServer, gvi, gvp, gcf, gsd);
22050
+ registerGraphAnalysisTools(targetServer, gvi, gvp, gsd, gcf);
22051
+ registerSemanticAnalysisTools(targetServer, gvi, gvp);
22052
+ registerVaultSchemaTools(targetServer, gvi, gvp);
22053
+ registerNoteIntelligenceTools(targetServer, gvi, gvp, gcf);
22054
+ registerMigrationTools(targetServer, gvi, gvp);
22055
+ registerMutationTools(targetServer, gvp, gcf);
22056
+ registerTaskTools(targetServer, gvp);
22057
+ registerFrontmatterTools(targetServer, gvp);
22058
+ registerNoteTools(targetServer, gvp, gvi);
22059
+ registerMoveNoteTools(targetServer, gvp);
22060
+ registerMergeTools(targetServer, gvp);
22061
+ registerSystemTools2(targetServer, gvp);
22062
+ registerPolicyTools(targetServer, gvp, () => {
22063
+ const index = gvi();
22064
+ if (!index) return void 0;
21774
22065
  return ({ query, folder, where, limit = 10 }) => {
21775
22066
  let notes = Array.from(index.notes.values());
21776
22067
  if (folder) {
@@ -21798,42 +22089,42 @@ function registerAllTools(targetServer) {
21798
22089
  }));
21799
22090
  };
21800
22091
  });
21801
- registerTagTools(targetServer, () => vaultIndex, () => vaultPath);
21802
- registerWikilinkFeedbackTools(targetServer, () => stateDb);
21803
- registerCorrectionTools(targetServer, () => stateDb);
21804
- registerInitTools(targetServer, () => vaultPath, () => stateDb);
22092
+ registerTagTools(targetServer, gvi, gvp);
22093
+ registerWikilinkFeedbackTools(targetServer, gsd);
22094
+ registerCorrectionTools(targetServer, gsd);
22095
+ registerInitTools(targetServer, gvp, gsd);
21805
22096
  registerConfigTools(
21806
22097
  targetServer,
21807
- () => flywheelConfig,
22098
+ gcf,
21808
22099
  (newConfig) => {
21809
22100
  flywheelConfig = newConfig;
21810
22101
  setWikilinkConfig(newConfig);
21811
22102
  },
21812
- () => stateDb
22103
+ gsd
21813
22104
  );
21814
- registerMetricsTools(targetServer, () => vaultIndex, () => stateDb);
21815
- registerActivityTools(targetServer, () => stateDb, () => {
22105
+ registerMetricsTools(targetServer, gvi, gsd);
22106
+ registerActivityTools(targetServer, gsd, () => {
21816
22107
  try {
21817
22108
  return getSessionId();
21818
22109
  } catch {
21819
22110
  return null;
21820
22111
  }
21821
22112
  });
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);
22113
+ registerSimilarityTools(targetServer, gvi, gvp, gsd);
22114
+ registerSemanticTools(targetServer, gvp, gsd);
22115
+ registerMergeTools2(targetServer, gsd);
22116
+ registerTemporalAnalysisTools(targetServer, gvi, gvp, gsd);
22117
+ registerMemoryTools(targetServer, gsd);
22118
+ registerRecallTools(targetServer, gsd, gvp, () => gvi() ?? null);
22119
+ registerBriefTools(targetServer, gsd);
22120
+ registerVaultResources(targetServer, () => gvi() ?? null);
21830
22121
  }
21831
22122
  function createConfiguredServer() {
21832
22123
  const s = new McpServer(
21833
22124
  { name: "flywheel-memory", version: pkg.version },
21834
22125
  { instructions: generateInstructions(enabledCategories, vaultRegistry) }
21835
22126
  );
21836
- applyToolGating(s, enabledCategories, () => stateDb, vaultRegistry);
22127
+ applyToolGating(s, enabledCategories, () => stateDb, vaultRegistry, () => vaultPath);
21837
22128
  registerAllTools(s);
21838
22129
  return s;
21839
22130
  }
@@ -21841,7 +22132,7 @@ var server = new McpServer(
21841
22132
  { name: "flywheel-memory", version: pkg.version },
21842
22133
  { instructions: generateInstructions(enabledCategories, vaultRegistry) }
21843
22134
  );
21844
- var _gatingResult = applyToolGating(server, enabledCategories, () => stateDb, vaultRegistry);
22135
+ var _gatingResult = applyToolGating(server, enabledCategories, () => stateDb, vaultRegistry, () => vaultPath);
21845
22136
  registerAllTools(server);
21846
22137
  var categoryList = Array.from(enabledCategories).sort().join(", ");
21847
22138
  serverLog("server", `Tool categories: ${categoryList}`);
@@ -21875,6 +22166,20 @@ async function initializeVault(name, vaultPathArg) {
21875
22166
  }
21876
22167
  return ctx;
21877
22168
  }
22169
+ function buildVaultScope(ctx) {
22170
+ return {
22171
+ name: ctx.name,
22172
+ vaultPath: ctx.vaultPath,
22173
+ stateDb: ctx.stateDb,
22174
+ flywheelConfig: ctx.flywheelConfig,
22175
+ vaultIndex: ctx.vaultIndex,
22176
+ cooccurrenceIndex: ctx.cooccurrenceIndex,
22177
+ indexState: ctx.indexState,
22178
+ indexError: ctx.indexError,
22179
+ embeddingsBuilding: ctx.embeddingsBuilding,
22180
+ entityEmbeddingsMap: getEntityEmbeddingsMap()
22181
+ };
22182
+ }
21878
22183
  function activateVault(ctx) {
21879
22184
  globalThis.__flywheel_active_vault = ctx.name;
21880
22185
  if (ctx.stateDb) {
@@ -21891,16 +22196,7 @@ function activateVault(ctx) {
21891
22196
  setIndexState(ctx.indexState);
21892
22197
  setIndexError(ctx.indexError);
21893
22198
  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
- });
22199
+ setActiveScope(buildVaultScope(ctx));
21904
22200
  }
21905
22201
  function getActiveVaultContext() {
21906
22202
  if (!vaultRegistry) return null;
@@ -21967,6 +22263,10 @@ async function main() {
21967
22263
  const { createMcpExpressApp } = await import("@modelcontextprotocol/sdk/server/express.js");
21968
22264
  const { StreamableHTTPServerTransport } = await import("@modelcontextprotocol/sdk/server/streamableHttp.js");
21969
22265
  const httpPort = parseInt(process.env.FLYWHEEL_HTTP_PORT ?? "3111", 10);
22266
+ if (!Number.isFinite(httpPort) || httpPort < 1 || httpPort > 65535) {
22267
+ console.error(`[flywheel] Fatal: invalid FLYWHEEL_HTTP_PORT: ${process.env.FLYWHEEL_HTTP_PORT} (must be 1-65535)`);
22268
+ process.exit(1);
22269
+ }
21970
22270
  const httpHost = process.env.FLYWHEEL_HTTP_HOST ?? "127.0.0.1";
21971
22271
  const app = createMcpExpressApp({ host: httpHost });
21972
22272
  app.post("/mcp", async (req, res) => {
@@ -22139,6 +22439,7 @@ function runPeriodicMaintenance(db4) {
22139
22439
  purgeOldSuggestionEvents(db4, 30);
22140
22440
  purgeOldNoteLinkHistory(db4, 90);
22141
22441
  purgeOldSnapshots(db4, 90);
22442
+ pruneStaleRetrievalCooccurrence(db4, 30);
22142
22443
  lastPurgeAt = now;
22143
22444
  serverLog("server", "Daily purge complete");
22144
22445
  }
@@ -23012,6 +23313,21 @@ async function runPostIndexWork(index) {
23012
23313
  tracker.end({ error: String(e) });
23013
23314
  serverLog("watcher", `Tag scan: failed: ${e}`, "error");
23014
23315
  }
23316
+ tracker.start("retrieval_cooccurrence", {});
23317
+ try {
23318
+ if (stateDb) {
23319
+ const inserted = mineRetrievalCooccurrence(stateDb);
23320
+ tracker.end({ pairs_inserted: inserted });
23321
+ if (inserted > 0) {
23322
+ serverLog("watcher", `Retrieval co-occurrence: ${inserted} new pairs`);
23323
+ }
23324
+ } else {
23325
+ tracker.end({ skipped: "no stateDb" });
23326
+ }
23327
+ } catch (e) {
23328
+ tracker.end({ error: String(e) });
23329
+ serverLog("watcher", `Retrieval co-occurrence: failed: ${e}`, "error");
23330
+ }
23015
23331
  const duration = Date.now() - batchStart;
23016
23332
  if (stateDb) {
23017
23333
  recordIndexEvent(stateDb, {