@velvetmonkey/flywheel-memory 2.0.51 → 2.0.52

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 +287 -132
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -1834,6 +1834,15 @@ var embeddingsBuilding = false;
1834
1834
  var embeddingCache = /* @__PURE__ */ new Map();
1835
1835
  var EMBEDDING_CACHE_MAX = 500;
1836
1836
  var entityEmbeddingsMap = /* @__PURE__ */ new Map();
1837
+ function getEmbeddingsBuildState() {
1838
+ if (!db) return "none";
1839
+ const row = db.prepare(`SELECT value FROM fts_metadata WHERE key = 'embeddings_state'`).get();
1840
+ return row?.value || "none";
1841
+ }
1842
+ function setEmbeddingsBuildState(state2) {
1843
+ if (!db) return;
1844
+ db.prepare(`INSERT OR REPLACE INTO fts_metadata (key, value) VALUES ('embeddings_state', ?)`).run(state2);
1845
+ }
1837
1846
  function setEmbeddingsDatabase(database) {
1838
1847
  db = database;
1839
1848
  }
@@ -1887,6 +1896,7 @@ async function buildEmbeddingsIndex(vaultPath2, onProgress) {
1887
1896
  throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
1888
1897
  }
1889
1898
  embeddingsBuilding = true;
1899
+ setEmbeddingsBuildState("building_notes");
1890
1900
  await initEmbeddings();
1891
1901
  const files = await scanVault(vaultPath2);
1892
1902
  const indexable = files.filter((f) => shouldIndexFile(f.path));
@@ -2037,8 +2047,13 @@ function setEmbeddingsBuilding(value) {
2037
2047
  function hasEmbeddingsIndex() {
2038
2048
  if (!db) return false;
2039
2049
  try {
2040
- const row = db.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
2041
- return row.count > 0;
2050
+ const state2 = getEmbeddingsBuildState();
2051
+ if (state2 === "complete") return true;
2052
+ if (state2 === "none") {
2053
+ const row = db.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
2054
+ return row.count > 0;
2055
+ }
2056
+ return false;
2042
2057
  } catch {
2043
2058
  return false;
2044
2059
  }
@@ -2090,6 +2105,7 @@ async function buildEntityEmbeddingsIndex(vaultPath2, entities, onProgress) {
2090
2105
  throw new Error("Embeddings database not initialized. Call setEmbeddingsDatabase() first.");
2091
2106
  }
2092
2107
  await initEmbeddings();
2108
+ setEmbeddingsBuildState("building_entities");
2093
2109
  const existingHashes = /* @__PURE__ */ new Map();
2094
2110
  const rows = db.prepare("SELECT entity_name, source_hash FROM entity_embeddings").all();
2095
2111
  for (const row of rows) {
@@ -3915,6 +3931,20 @@ function processImplicitFeedback(stateDb2, notePath, currentContent) {
3915
3931
  if (removed.length > 0) {
3916
3932
  updateSuppressionList(stateDb2);
3917
3933
  }
3934
+ const SURVIVAL_COOLDOWN_MS = 24 * 60 * 60 * 1e3;
3935
+ const getLastSurvival = stateDb2.db.prepare(
3936
+ `SELECT MAX(created_at) as last FROM wikilink_feedback
3937
+ WHERE entity = ? COLLATE NOCASE AND context = 'implicit:survived' AND note_path = ?`
3938
+ );
3939
+ for (const { entity } of trackedWithTime) {
3940
+ if (currentLinks.has(entity.toLowerCase())) {
3941
+ const lastSurvival = getLastSurvival.get(entity, notePath);
3942
+ const lastAt = lastSurvival?.last ? new Date(lastSurvival.last).getTime() : 0;
3943
+ if (Date.now() - lastAt > SURVIVAL_COOLDOWN_MS) {
3944
+ recordFeedback(stateDb2, entity, "implicit:survived", notePath, true, 0.8);
3945
+ }
3946
+ }
3947
+ }
3918
3948
  return removed;
3919
3949
  }
3920
3950
  var TIER_LABELS = [
@@ -5534,6 +5564,68 @@ function tokenIdf(token, coocIndex) {
5534
5564
  const rawIdf = Math.log((N + 1) / (df + 1));
5535
5565
  return Math.max(0.5, Math.min(2.5, rawIdf));
5536
5566
  }
5567
+ function serializeCooccurrenceIndex(index) {
5568
+ const serialized = {};
5569
+ for (const [entity, assocs] of Object.entries(index.associations)) {
5570
+ serialized[entity] = Object.fromEntries(assocs);
5571
+ }
5572
+ return {
5573
+ associations: serialized,
5574
+ minCount: index.minCount,
5575
+ documentFrequency: Object.fromEntries(index.documentFrequency),
5576
+ totalNotesScanned: index.totalNotesScanned,
5577
+ _metadata: index._metadata
5578
+ };
5579
+ }
5580
+ function deserializeCooccurrenceIndex(data) {
5581
+ try {
5582
+ const associations = {};
5583
+ const assocData = data.associations;
5584
+ if (!assocData) return null;
5585
+ for (const [entity, assocs] of Object.entries(assocData)) {
5586
+ associations[entity] = new Map(Object.entries(assocs));
5587
+ }
5588
+ const dfData = data.documentFrequency;
5589
+ const documentFrequency = dfData ? new Map(Object.entries(dfData).map(([k, v]) => [k, v])) : /* @__PURE__ */ new Map();
5590
+ const totalNotesScanned = data.totalNotesScanned || 0;
5591
+ return {
5592
+ associations,
5593
+ minCount: data.minCount || DEFAULT_MIN_COOCCURRENCE,
5594
+ documentFrequency,
5595
+ totalNotesScanned,
5596
+ _metadata: data._metadata
5597
+ };
5598
+ } catch {
5599
+ return null;
5600
+ }
5601
+ }
5602
+ var COOCCURRENCE_CACHE_MAX_AGE_MS = 60 * 60 * 1e3;
5603
+ function saveCooccurrenceToStateDb(stateDb2, index) {
5604
+ const serialized = serializeCooccurrenceIndex(index);
5605
+ const data = JSON.stringify(serialized);
5606
+ const entityCount = Object.keys(index.associations).length;
5607
+ const associationCount = index._metadata.total_associations;
5608
+ stateDb2.db.prepare(`
5609
+ INSERT OR REPLACE INTO cooccurrence_cache (id, data, built_at, entity_count, association_count)
5610
+ VALUES (1, ?, ?, ?, ?)
5611
+ `).run(data, Date.now(), entityCount, associationCount);
5612
+ }
5613
+ function loadCooccurrenceFromStateDb(stateDb2) {
5614
+ const row = stateDb2.db.prepare(
5615
+ "SELECT data, built_at FROM cooccurrence_cache WHERE id = 1"
5616
+ ).get();
5617
+ if (!row) return null;
5618
+ const ageMs = Date.now() - row.built_at;
5619
+ if (ageMs > COOCCURRENCE_CACHE_MAX_AGE_MS) return null;
5620
+ try {
5621
+ const parsed = JSON.parse(row.data);
5622
+ const index = deserializeCooccurrenceIndex(parsed);
5623
+ if (!index) return null;
5624
+ return { index, builtAt: row.built_at };
5625
+ } catch {
5626
+ return null;
5627
+ }
5628
+ }
5537
5629
 
5538
5630
  // src/core/write/edgeWeights.ts
5539
5631
  var moduleStateDb4 = null;
@@ -7909,7 +8001,7 @@ function registerGraphTools(server2, getIndex, getVaultPath, getStateDb) {
7909
8001
 
7910
8002
  // src/tools/read/wikilinks.ts
7911
8003
  import { z as z2 } from "zod";
7912
- import { detectImplicitEntities as detectImplicitEntities2 } from "@velvetmonkey/vault-core";
8004
+ import { detectImplicitEntities as detectImplicitEntities2, IMPLICIT_EXCLUDE_WORDS } from "@velvetmonkey/vault-core";
7913
8005
  function findEntityMatches(text, entities) {
7914
8006
  const matches = [];
7915
8007
  const sortedEntities = Array.from(entities.entries()).filter(([name]) => name.length >= 2).sort((a, b) => b[0].length - a[0].length);
@@ -8031,6 +8123,8 @@ function registerWikilinkTools(server2, getIndex, getVaultPath) {
8031
8123
  if (links.length < 2) continue;
8032
8124
  if (index.entities.has(target.toLowerCase())) continue;
8033
8125
  if (linkedSet.has(target.toLowerCase())) continue;
8126
+ if (IMPLICIT_EXCLUDE_WORDS.has(target.toLowerCase())) continue;
8127
+ if (target.length < 4) continue;
8034
8128
  const targetLower = target.toLowerCase();
8035
8129
  const textLower = text.toLowerCase();
8036
8130
  let searchPos = 0;
@@ -16294,6 +16388,24 @@ function resolveCorrection(stateDb2, id, newStatus) {
16294
16388
  `).run(newStatus, id);
16295
16389
  return result.changes > 0;
16296
16390
  }
16391
+ function processPendingCorrections(stateDb2) {
16392
+ const pending = listCorrections(stateDb2, "pending");
16393
+ let processed = 0;
16394
+ for (const correction of pending) {
16395
+ if (!correction.entity) {
16396
+ resolveCorrection(stateDb2, correction.id, "dismissed");
16397
+ continue;
16398
+ }
16399
+ if (correction.correction_type === "wrong_link") {
16400
+ recordFeedback(stateDb2, correction.entity, "correction:wrong_link", correction.note_path || "", false, 1);
16401
+ } else if (correction.correction_type === "wrong_category") {
16402
+ recordFeedback(stateDb2, correction.entity, "correction:wrong_category", "", false, 0.5);
16403
+ }
16404
+ resolveCorrection(stateDb2, correction.id, "applied");
16405
+ processed++;
16406
+ }
16407
+ return processed;
16408
+ }
16297
16409
 
16298
16410
  // src/tools/write/corrections.ts
16299
16411
  function registerCorrectionTools(server2, getStateDb) {
@@ -18926,6 +19038,12 @@ async function main() {
18926
19038
  setWriteStateDb(stateDb);
18927
19039
  setRecencyStateDb(stateDb);
18928
19040
  setEdgeWeightStateDb(stateDb);
19041
+ const cachedCooc = loadCooccurrenceFromStateDb(stateDb);
19042
+ if (cachedCooc) {
19043
+ setCooccurrenceIndex(cachedCooc.index);
19044
+ lastCooccurrenceRebuildAt = cachedCooc.builtAt;
19045
+ serverLog("index", `Co-occurrence: loaded from cache (${Object.keys(cachedCooc.index.associations).length} entities, ${cachedCooc.index._metadata.total_associations} associations)`);
19046
+ }
18929
19047
  const vaultInitRow = stateDb.getMetadataValue.get("vault_init_last_run_at");
18930
19048
  if (!vaultInitRow) {
18931
19049
  serverLog("server", "Vault not initialized \u2014 call vault_init to enrich legacy notes");
@@ -19154,6 +19272,7 @@ async function runPostIndexWork(index) {
19154
19272
  }
19155
19273
  }
19156
19274
  loadEntityEmbeddingsToMemory();
19275
+ setEmbeddingsBuildState("complete");
19157
19276
  serverLog("semantic", "Embeddings ready");
19158
19277
  }).catch((err) => {
19159
19278
  serverLog("semantic", `Embeddings build failed: ${err instanceof Error ? err.message : err}`, "error");
@@ -19339,17 +19458,22 @@ async function runPostIndexWork(index) {
19339
19458
  tracker.end({ entity_count: entitiesAfter.length, ...entityDiff, category_changes: categoryChanges, description_changes: descriptionChanges });
19340
19459
  serverLog("watcher", `Entity scan: ${entitiesAfter.length} entities`);
19341
19460
  tracker.start("hub_scores", { entity_count: entitiesAfter.length });
19342
- const hubUpdated = await exportHubScores(vaultIndex, stateDb);
19343
- const hubDiffs = [];
19344
- if (stateDb) {
19345
- const rows = stateDb.db.prepare("SELECT name, hub_score FROM entities").all();
19346
- for (const r of rows) {
19347
- const prev = hubBefore.get(r.name) ?? 0;
19348
- if (prev !== r.hub_score) hubDiffs.push({ entity: r.name, before: prev, after: r.hub_score });
19461
+ try {
19462
+ const hubUpdated = await exportHubScores(vaultIndex, stateDb);
19463
+ const hubDiffs = [];
19464
+ if (stateDb) {
19465
+ const rows = stateDb.db.prepare("SELECT name, hub_score FROM entities").all();
19466
+ for (const r of rows) {
19467
+ const prev = hubBefore.get(r.name) ?? 0;
19468
+ if (prev !== r.hub_score) hubDiffs.push({ entity: r.name, before: prev, after: r.hub_score });
19469
+ }
19349
19470
  }
19471
+ tracker.end({ updated: hubUpdated ?? 0, diffs: hubDiffs.slice(0, 10) });
19472
+ serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
19473
+ } catch (e) {
19474
+ tracker.end({ error: String(e) });
19475
+ serverLog("watcher", `Hub scores: failed: ${e}`, "error");
19350
19476
  }
19351
- tracker.end({ updated: hubUpdated ?? 0, diffs: hubDiffs.slice(0, 10) });
19352
- serverLog("watcher", `Hub scores: ${hubUpdated ?? 0} updated`);
19353
19477
  tracker.start("recency", { entity_count: entitiesAfter.length });
19354
19478
  try {
19355
19479
  const cachedRecency = loadRecencyFromStateDb();
@@ -19376,6 +19500,9 @@ async function runPostIndexWork(index) {
19376
19500
  const cooccurrenceIdx = await mineCooccurrences(vaultPath, entityNames);
19377
19501
  setCooccurrenceIndex(cooccurrenceIdx);
19378
19502
  lastCooccurrenceRebuildAt = Date.now();
19503
+ if (stateDb) {
19504
+ saveCooccurrenceToStateDb(stateDb, cooccurrenceIdx);
19505
+ }
19379
19506
  tracker.end({ rebuilt: true, associations: cooccurrenceIdx._metadata.total_associations });
19380
19507
  serverLog("watcher", `Co-occurrence: rebuilt ${cooccurrenceIdx._metadata.total_associations} associations`);
19381
19508
  } else {
@@ -19491,87 +19618,99 @@ async function runPostIndexWork(index) {
19491
19618
  }
19492
19619
  tracker.end({ updated: taskUpdated, removed: taskRemoved });
19493
19620
  serverLog("watcher", `Task cache: ${taskUpdated} updated, ${taskRemoved} removed`);
19494
- tracker.start("forward_links", { files: filteredEvents.length });
19495
- const eventTypeMap = new Map(filteredEvents.map((e) => [e.path, e.type]));
19496
19621
  const forwardLinkResults = [];
19497
19622
  let totalResolved = 0;
19498
19623
  let totalDead = 0;
19499
- for (const event of filteredEvents) {
19500
- if (event.type === "delete" || !event.path.endsWith(".md")) continue;
19501
- try {
19502
- const links = getForwardLinksForNote(vaultIndex, event.path);
19503
- const resolved = [];
19504
- const dead = [];
19505
- const seen = /* @__PURE__ */ new Set();
19506
- for (const link of links) {
19507
- const name = link.target;
19508
- if (seen.has(name.toLowerCase())) continue;
19509
- seen.add(name.toLowerCase());
19510
- if (link.exists) resolved.push(name);
19511
- else dead.push(name);
19512
- }
19513
- if (resolved.length > 0 || dead.length > 0) {
19514
- forwardLinkResults.push({ file: event.path, resolved, dead });
19515
- }
19516
- totalResolved += resolved.length;
19517
- totalDead += dead.length;
19518
- } catch {
19519
- }
19520
- }
19521
19624
  const linkDiffs = [];
19522
19625
  const survivedLinks = [];
19523
- if (stateDb) {
19524
- const upsertHistory = stateDb.db.prepare(`
19626
+ tracker.start("forward_links", { files: filteredEvents.length });
19627
+ try {
19628
+ const eventTypeMap = new Map(filteredEvents.map((e) => [e.path, e.type]));
19629
+ for (const event of filteredEvents) {
19630
+ if (event.type === "delete" || !event.path.endsWith(".md")) continue;
19631
+ try {
19632
+ const links = getForwardLinksForNote(vaultIndex, event.path);
19633
+ const resolved = [];
19634
+ const dead = [];
19635
+ const seen = /* @__PURE__ */ new Set();
19636
+ for (const link of links) {
19637
+ const name = link.target;
19638
+ if (seen.has(name.toLowerCase())) continue;
19639
+ seen.add(name.toLowerCase());
19640
+ if (link.exists) resolved.push(name);
19641
+ else dead.push(name);
19642
+ }
19643
+ if (resolved.length > 0 || dead.length > 0) {
19644
+ forwardLinkResults.push({ file: event.path, resolved, dead });
19645
+ }
19646
+ totalResolved += resolved.length;
19647
+ totalDead += dead.length;
19648
+ } catch {
19649
+ }
19650
+ }
19651
+ if (stateDb) {
19652
+ const upsertHistory = stateDb.db.prepare(`
19525
19653
  INSERT INTO note_link_history (note_path, target) VALUES (?, ?)
19526
19654
  ON CONFLICT(note_path, target) DO UPDATE SET edits_survived = edits_survived + 1
19527
19655
  `);
19528
- const checkThreshold = stateDb.db.prepare(`
19656
+ const checkThreshold = stateDb.db.prepare(`
19529
19657
  SELECT target FROM note_link_history
19530
19658
  WHERE note_path = ? AND target = ? AND edits_survived >= 3 AND last_positive_at IS NULL
19531
19659
  `);
19532
- const markPositive = stateDb.db.prepare(`
19660
+ const markPositive = stateDb.db.prepare(`
19533
19661
  UPDATE note_link_history SET last_positive_at = datetime('now') WHERE note_path = ? AND target = ?
19534
19662
  `);
19535
- const getEdgeCount = stateDb.db.prepare(
19536
- "SELECT edits_survived FROM note_link_history WHERE note_path=? AND target=?"
19537
- );
19538
- for (const entry of forwardLinkResults) {
19539
- const currentSet = /* @__PURE__ */ new Set([
19540
- ...entry.resolved.map((n) => n.toLowerCase()),
19541
- ...entry.dead.map((n) => n.toLowerCase())
19542
- ]);
19543
- const previousSet = getStoredNoteLinks(stateDb, entry.file);
19544
- if (previousSet.size === 0) {
19663
+ const getEdgeCount = stateDb.db.prepare(
19664
+ "SELECT edits_survived FROM note_link_history WHERE note_path=? AND target=?"
19665
+ );
19666
+ for (const entry of forwardLinkResults) {
19667
+ const currentSet = /* @__PURE__ */ new Set([
19668
+ ...entry.resolved.map((n) => n.toLowerCase()),
19669
+ ...entry.dead.map((n) => n.toLowerCase())
19670
+ ]);
19671
+ const previousSet = getStoredNoteLinks(stateDb, entry.file);
19672
+ if (previousSet.size === 0) {
19673
+ updateStoredNoteLinks(stateDb, entry.file, currentSet);
19674
+ continue;
19675
+ }
19676
+ const diff = diffNoteLinks(previousSet, currentSet);
19677
+ if (diff.added.length > 0 || diff.removed.length > 0) {
19678
+ linkDiffs.push({ file: entry.file, ...diff });
19679
+ }
19545
19680
  updateStoredNoteLinks(stateDb, entry.file, currentSet);
19546
- continue;
19547
- }
19548
- const diff = diffNoteLinks(previousSet, currentSet);
19549
- if (diff.added.length > 0 || diff.removed.length > 0) {
19550
- linkDiffs.push({ file: entry.file, ...diff });
19551
- }
19552
- updateStoredNoteLinks(stateDb, entry.file, currentSet);
19553
- if (diff.removed.length === 0) continue;
19554
- for (const link of currentSet) {
19555
- if (!previousSet.has(link)) continue;
19556
- upsertHistory.run(entry.file, link);
19557
- const countRow = getEdgeCount.get(entry.file, link);
19558
- if (countRow) {
19559
- survivedLinks.push({ entity: link, file: entry.file, count: countRow.edits_survived });
19681
+ if (diff.removed.length === 0) continue;
19682
+ for (const link of currentSet) {
19683
+ if (!previousSet.has(link)) continue;
19684
+ upsertHistory.run(entry.file, link);
19685
+ const countRow = getEdgeCount.get(entry.file, link);
19686
+ if (countRow) {
19687
+ survivedLinks.push({ entity: link, file: entry.file, count: countRow.edits_survived });
19688
+ }
19689
+ const hit = checkThreshold.get(entry.file, link);
19690
+ if (hit) {
19691
+ const entity = entitiesAfter.find(
19692
+ (e) => e.nameLower === link || (e.aliases ?? []).some((a) => a.toLowerCase() === link)
19693
+ );
19694
+ if (entity) {
19695
+ recordFeedback(stateDb, entity.name, "implicit:kept", entry.file, true, 0.8);
19696
+ markPositive.run(entry.file, link);
19697
+ }
19698
+ }
19560
19699
  }
19561
- const hit = checkThreshold.get(entry.file, link);
19562
- if (hit) {
19563
- const entity = entitiesAfter.find(
19564
- (e) => e.nameLower === link || (e.aliases ?? []).some((a) => a.toLowerCase() === link)
19565
- );
19566
- if (entity) {
19567
- recordFeedback(stateDb, entity.name, "implicit:kept", entry.file, true, 0.8);
19568
- markPositive.run(entry.file, link);
19700
+ }
19701
+ for (const event of filteredEvents) {
19702
+ if (event.type === "delete") {
19703
+ const previousSet = getStoredNoteLinks(stateDb, event.path);
19704
+ if (previousSet.size > 0) {
19705
+ linkDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
19706
+ updateStoredNoteLinks(stateDb, event.path, /* @__PURE__ */ new Set());
19569
19707
  }
19570
19708
  }
19571
19709
  }
19572
- }
19573
- for (const event of filteredEvents) {
19574
- if (event.type === "delete") {
19710
+ const processedFiles = new Set(forwardLinkResults.map((r) => r.file));
19711
+ for (const event of filteredEvents) {
19712
+ if (event.type === "delete" || !event.path.endsWith(".md")) continue;
19713
+ if (processedFiles.has(event.path)) continue;
19575
19714
  const previousSet = getStoredNoteLinks(stateDb, event.path);
19576
19715
  if (previousSet.size > 0) {
19577
19716
  linkDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
@@ -19579,33 +19718,26 @@ async function runPostIndexWork(index) {
19579
19718
  }
19580
19719
  }
19581
19720
  }
19582
- const processedFiles = new Set(forwardLinkResults.map((r) => r.file));
19583
- for (const event of filteredEvents) {
19584
- if (event.type === "delete" || !event.path.endsWith(".md")) continue;
19585
- if (processedFiles.has(event.path)) continue;
19586
- const previousSet = getStoredNoteLinks(stateDb, event.path);
19587
- if (previousSet.size > 0) {
19588
- linkDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
19589
- updateStoredNoteLinks(stateDb, event.path, /* @__PURE__ */ new Set());
19721
+ const newDeadLinks = [];
19722
+ for (const diff of linkDiffs) {
19723
+ const newDead = diff.added.filter((target) => !vaultIndex.entities.has(target.toLowerCase()));
19724
+ if (newDead.length > 0) {
19725
+ newDeadLinks.push({ file: diff.file, targets: newDead });
19590
19726
  }
19591
19727
  }
19728
+ tracker.end({
19729
+ total_resolved: totalResolved,
19730
+ total_dead: totalDead,
19731
+ links: forwardLinkResults,
19732
+ link_diffs: linkDiffs,
19733
+ survived: survivedLinks,
19734
+ new_dead_links: newDeadLinks
19735
+ });
19736
+ serverLog("watcher", `Forward links: ${totalResolved} resolved, ${totalDead} dead${newDeadLinks.length > 0 ? `, ${newDeadLinks.reduce((s, d) => s + d.targets.length, 0)} new dead` : ""}`);
19737
+ } catch (e) {
19738
+ tracker.end({ error: String(e) });
19739
+ serverLog("watcher", `Forward links: failed: ${e}`, "error");
19592
19740
  }
19593
- const newDeadLinks = [];
19594
- for (const diff of linkDiffs) {
19595
- const newDead = diff.added.filter((target) => !vaultIndex.entities.has(target.toLowerCase()));
19596
- if (newDead.length > 0) {
19597
- newDeadLinks.push({ file: diff.file, targets: newDead });
19598
- }
19599
- }
19600
- tracker.end({
19601
- total_resolved: totalResolved,
19602
- total_dead: totalDead,
19603
- links: forwardLinkResults,
19604
- link_diffs: linkDiffs,
19605
- survived: survivedLinks,
19606
- new_dead_links: newDeadLinks
19607
- });
19608
- serverLog("watcher", `Forward links: ${totalResolved} resolved, ${totalDead} dead${newDeadLinks.length > 0 ? `, ${newDeadLinks.reduce((s, d) => s + d.targets.length, 0)} new dead` : ""}`);
19609
19741
  tracker.start("wikilink_check", { files: filteredEvents.length });
19610
19742
  const trackedLinks = [];
19611
19743
  if (stateDb) {
@@ -19730,6 +19862,24 @@ async function runPostIndexWork(index) {
19730
19862
  if (newlySuppressed.length > 0) {
19731
19863
  serverLog("watcher", `Suppression: ${newlySuppressed.length} entities newly suppressed: ${newlySuppressed.join(", ")}`);
19732
19864
  }
19865
+ tracker.start("corrections", {});
19866
+ try {
19867
+ if (stateDb) {
19868
+ const corrProcessed = processPendingCorrections(stateDb);
19869
+ if (corrProcessed > 0) {
19870
+ updateSuppressionList(stateDb);
19871
+ }
19872
+ tracker.end({ processed: corrProcessed });
19873
+ if (corrProcessed > 0) {
19874
+ serverLog("watcher", `Corrections: ${corrProcessed} processed`);
19875
+ }
19876
+ } else {
19877
+ tracker.end({ skipped: true });
19878
+ }
19879
+ } catch (e) {
19880
+ tracker.end({ error: String(e) });
19881
+ serverLog("watcher", `Corrections: failed: ${e}`, "error");
19882
+ }
19733
19883
  tracker.start("prospect_scan", { files: filteredEvents.length });
19734
19884
  const prospectResults = [];
19735
19885
  for (const event of filteredEvents) {
@@ -19793,45 +19943,50 @@ async function runPostIndexWork(index) {
19793
19943
  serverLog("watcher", `Suggestion scoring: ${suggestionResults.length} files scored`);
19794
19944
  }
19795
19945
  tracker.start("tag_scan", { files: filteredEvents.length });
19796
- const tagDiffs = [];
19797
- if (stateDb) {
19798
- const noteTagsForward = /* @__PURE__ */ new Map();
19799
- for (const [tag, paths] of vaultIndex.tags) {
19800
- for (const notePath of paths) {
19801
- if (!noteTagsForward.has(notePath)) noteTagsForward.set(notePath, /* @__PURE__ */ new Set());
19802
- noteTagsForward.get(notePath).add(tag);
19946
+ try {
19947
+ const tagDiffs = [];
19948
+ if (stateDb) {
19949
+ const noteTagsForward = /* @__PURE__ */ new Map();
19950
+ for (const [tag, paths] of vaultIndex.tags) {
19951
+ for (const notePath of paths) {
19952
+ if (!noteTagsForward.has(notePath)) noteTagsForward.set(notePath, /* @__PURE__ */ new Set());
19953
+ noteTagsForward.get(notePath).add(tag);
19954
+ }
19803
19955
  }
19804
- }
19805
- for (const event of filteredEvents) {
19806
- if (event.type === "delete" || !event.path.endsWith(".md")) continue;
19807
- const currentSet = noteTagsForward.get(event.path) ?? /* @__PURE__ */ new Set();
19808
- const previousSet = getStoredNoteTags(stateDb, event.path);
19809
- if (previousSet.size === 0 && currentSet.size > 0) {
19956
+ for (const event of filteredEvents) {
19957
+ if (event.type === "delete" || !event.path.endsWith(".md")) continue;
19958
+ const currentSet = noteTagsForward.get(event.path) ?? /* @__PURE__ */ new Set();
19959
+ const previousSet = getStoredNoteTags(stateDb, event.path);
19960
+ if (previousSet.size === 0 && currentSet.size > 0) {
19961
+ updateStoredNoteTags(stateDb, event.path, currentSet);
19962
+ continue;
19963
+ }
19964
+ const added = [...currentSet].filter((t) => !previousSet.has(t));
19965
+ const removed = [...previousSet].filter((t) => !currentSet.has(t));
19966
+ if (added.length > 0 || removed.length > 0) {
19967
+ tagDiffs.push({ file: event.path, added, removed });
19968
+ }
19810
19969
  updateStoredNoteTags(stateDb, event.path, currentSet);
19811
- continue;
19812
- }
19813
- const added = [...currentSet].filter((t) => !previousSet.has(t));
19814
- const removed = [...previousSet].filter((t) => !currentSet.has(t));
19815
- if (added.length > 0 || removed.length > 0) {
19816
- tagDiffs.push({ file: event.path, added, removed });
19817
19970
  }
19818
- updateStoredNoteTags(stateDb, event.path, currentSet);
19819
- }
19820
- for (const event of filteredEvents) {
19821
- if (event.type === "delete") {
19822
- const previousSet = getStoredNoteTags(stateDb, event.path);
19823
- if (previousSet.size > 0) {
19824
- tagDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
19825
- updateStoredNoteTags(stateDb, event.path, /* @__PURE__ */ new Set());
19971
+ for (const event of filteredEvents) {
19972
+ if (event.type === "delete") {
19973
+ const previousSet = getStoredNoteTags(stateDb, event.path);
19974
+ if (previousSet.size > 0) {
19975
+ tagDiffs.push({ file: event.path, added: [], removed: [...previousSet] });
19976
+ updateStoredNoteTags(stateDb, event.path, /* @__PURE__ */ new Set());
19977
+ }
19826
19978
  }
19827
19979
  }
19828
19980
  }
19829
- }
19830
- const totalTagsAdded = tagDiffs.reduce((s, d) => s + d.added.length, 0);
19831
- const totalTagsRemoved = tagDiffs.reduce((s, d) => s + d.removed.length, 0);
19832
- tracker.end({ total_added: totalTagsAdded, total_removed: totalTagsRemoved, tag_diffs: tagDiffs });
19833
- if (tagDiffs.length > 0) {
19834
- serverLog("watcher", `Tag scan: ${totalTagsAdded} added, ${totalTagsRemoved} removed across ${tagDiffs.length} files`);
19981
+ const totalTagsAdded = tagDiffs.reduce((s, d) => s + d.added.length, 0);
19982
+ const totalTagsRemoved = tagDiffs.reduce((s, d) => s + d.removed.length, 0);
19983
+ tracker.end({ total_added: totalTagsAdded, total_removed: totalTagsRemoved, tag_diffs: tagDiffs });
19984
+ if (tagDiffs.length > 0) {
19985
+ serverLog("watcher", `Tag scan: ${totalTagsAdded} added, ${totalTagsRemoved} removed across ${tagDiffs.length} files`);
19986
+ }
19987
+ } catch (e) {
19988
+ tracker.end({ error: String(e) });
19989
+ serverLog("watcher", `Tag scan: failed: ${e}`, "error");
19835
19990
  }
19836
19991
  const duration = Date.now() - batchStart;
19837
19992
  if (stateDb) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.51",
3
+ "version": "2.0.52",
4
4
  "description": "MCP server that gives Claude full read/write access to your Obsidian vault. Select from 42 tools for search, backlinks, graph queries, mutations, and hybrid semantic search.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -52,7 +52,7 @@
52
52
  },
53
53
  "dependencies": {
54
54
  "@modelcontextprotocol/sdk": "^1.25.1",
55
- "@velvetmonkey/vault-core": "^2.0.50",
55
+ "@velvetmonkey/vault-core": "^2.0.52",
56
56
  "better-sqlite3": "^11.0.0",
57
57
  "chokidar": "^4.0.0",
58
58
  "gray-matter": "^4.0.3",