@velvetmonkey/flywheel-memory 2.0.156 → 2.0.158

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 +170 -36
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -557,6 +557,18 @@ function hasEmbeddingsIndex() {
557
557
  const row = db4.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
558
558
  return row.count > 0;
559
559
  }
560
+ if (!isEmbeddingsBuilding()) {
561
+ const row = db4.prepare("SELECT COUNT(*) as count FROM note_embeddings").get();
562
+ if (row.count > 0) {
563
+ setEmbeddingsBuildState("complete");
564
+ console.error("[Semantic] Recovered stale embeddings_state \u2192 complete");
565
+ return true;
566
+ } else {
567
+ setEmbeddingsBuildState("none");
568
+ console.error("[Semantic] Recovered stale embeddings_state \u2192 none (no rows)");
569
+ return false;
570
+ }
571
+ }
560
572
  return false;
561
573
  } catch {
562
574
  return false;
@@ -575,7 +587,7 @@ function getStoredEmbeddingModel() {
575
587
  function diagnoseEmbeddings(vaultPath2) {
576
588
  const db4 = getDb();
577
589
  const checks = [];
578
- const counts = { embedded: 0, vaultNotes: 0, orphaned: 0, missing: 0 };
590
+ const counts = { embedded: 0, vaultNotes: 0, orphaned: 0, orphanedEntities: 0, missing: 0 };
579
591
  if (!db4) {
580
592
  checks.push({ name: "database", status: "stale", detail: "No database available" });
581
593
  return { healthy: false, checks, counts };
@@ -649,6 +661,25 @@ function diagnoseEmbeddings(vaultPath2) {
649
661
  } catch {
650
662
  checks.push({ name: "orphans", status: "warning", detail: "Could not check" });
651
663
  }
664
+ try {
665
+ const embNames = new Set(
666
+ db4.prepare("SELECT entity_name FROM entity_embeddings").all().map((r) => r.entity_name)
667
+ );
668
+ const entityNames = new Set(
669
+ db4.prepare("SELECT name FROM entities").all().map((r) => r.name)
670
+ );
671
+ counts.orphanedEntities = 0;
672
+ for (const n of embNames) {
673
+ if (!entityNames.has(n)) counts.orphanedEntities++;
674
+ }
675
+ if (counts.orphanedEntities > 0) {
676
+ checks.push({ name: "entity_orphans", status: "warning", detail: `${counts.orphanedEntities} orphaned entity embeddings` });
677
+ } else {
678
+ checks.push({ name: "entity_orphans", status: "ok", detail: "0 orphaned" });
679
+ }
680
+ } catch {
681
+ checks.push({ name: "entity_orphans", status: "warning", detail: "Could not check entity embeddings" });
682
+ }
652
683
  try {
653
684
  const samples = db4.prepare("SELECT embedding FROM note_embeddings ORDER BY RANDOM() LIMIT 3").all();
654
685
  let corrupt = false;
@@ -732,6 +763,7 @@ async function buildEntityEmbeddingsIndex(vaultPath2, entities, onProgress) {
732
763
  const total = entities.size;
733
764
  let done = 0;
734
765
  let updated = 0;
766
+ let skipped = 0;
735
767
  for (const [name, entity] of entities) {
736
768
  done++;
737
769
  try {
@@ -745,7 +777,11 @@ async function buildEntityEmbeddingsIndex(vaultPath2, entities, onProgress) {
745
777
  const buf = Buffer.from(embedding.buffer, embedding.byteOffset, embedding.byteLength);
746
778
  upsert.run(name, buf, hash, activeModelConfig.id, Date.now());
747
779
  updated++;
748
- } catch {
780
+ } catch (err) {
781
+ skipped++;
782
+ if (skipped <= 3) {
783
+ console.error(`[Semantic] Failed to embed entity ${name}: ${err instanceof Error ? err.message : err}`);
784
+ }
749
785
  }
750
786
  if (onProgress) onProgress(done, total);
751
787
  }
@@ -755,7 +791,7 @@ async function buildEntityEmbeddingsIndex(vaultPath2, entities, onProgress) {
755
791
  deleteStmt.run(existingName);
756
792
  }
757
793
  }
758
- console.error(`[Semantic] Entity embeddings: ${updated} updated, ${total - updated} unchanged`);
794
+ console.error(`[Semantic] Entity embeddings: ${updated} updated, ${total - updated - skipped} unchanged, ${skipped} failed`);
759
795
  return updated;
760
796
  }
761
797
  async function updateEntityEmbedding(entityName, entity, vaultPath2) {
@@ -773,9 +809,34 @@ async function updateEntityEmbedding(entityName, entity, vaultPath2) {
773
809
  VALUES (?, ?, ?, ?, ?)
774
810
  `).run(entityName, buf, hash, activeModelConfig.id, Date.now());
775
811
  entityEmbeddingsMap.set(entityName, embedding);
776
- } catch {
812
+ } catch (err) {
813
+ console.error(`[Semantic] Failed to update entity embedding ${entityName}: ${err instanceof Error ? err.message : err}`);
777
814
  }
778
815
  }
816
+ function removeOrphanedNoteEmbeddings() {
817
+ const db4 = getDb();
818
+ if (!db4) return 0;
819
+ const result = db4.prepare(
820
+ "DELETE FROM note_embeddings WHERE path NOT IN (SELECT path FROM notes_fts)"
821
+ ).run();
822
+ return result.changes;
823
+ }
824
+ function removeOrphanedEntityEmbeddings(currentEntityNames) {
825
+ const db4 = getDb();
826
+ if (!db4) return 0;
827
+ const rows = db4.prepare("SELECT entity_name FROM entity_embeddings").all();
828
+ const deleteStmt = db4.prepare("DELETE FROM entity_embeddings WHERE entity_name = ?");
829
+ const embMap = getEmbMap();
830
+ let removed = 0;
831
+ for (const row of rows) {
832
+ if (!currentEntityNames.has(row.entity_name)) {
833
+ deleteStmt.run(row.entity_name);
834
+ embMap.delete(row.entity_name);
835
+ removed++;
836
+ }
837
+ }
838
+ return removed;
839
+ }
779
840
  function findSemanticallySimilarEntities(queryEmbedding, limit, excludeEntities) {
780
841
  const scored = [];
781
842
  for (const [entityName, embedding] of getEmbMap()) {
@@ -7652,6 +7713,38 @@ function purgeOldNoteLinkHistory(stateDb2, retentionDays = 90) {
7652
7713
  return result.changes;
7653
7714
  }
7654
7715
 
7716
+ // src/core/read/identity.ts
7717
+ function normalizeResolvedPath(notePath) {
7718
+ return notePath.toLowerCase().replace(/\.md$/, "");
7719
+ }
7720
+ function getInboundTargetsForNote(stateDb2, notePath) {
7721
+ const targets = /* @__PURE__ */ new Set();
7722
+ const stem2 = notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase();
7723
+ const normalizedPath = normalizeResolvedPath(notePath);
7724
+ if (stateDb2) {
7725
+ try {
7726
+ const row = stateDb2.db.prepare(
7727
+ `SELECT name_lower, aliases_json FROM entities
7728
+ WHERE LOWER(REPLACE(path, '.md', '')) = ?`
7729
+ ).get(normalizedPath);
7730
+ if (row) {
7731
+ targets.add(row.name_lower);
7732
+ if (row.aliases_json) {
7733
+ try {
7734
+ for (const alias of JSON.parse(row.aliases_json)) {
7735
+ targets.add(alias.toLowerCase());
7736
+ }
7737
+ } catch {
7738
+ }
7739
+ }
7740
+ }
7741
+ } catch {
7742
+ }
7743
+ }
7744
+ if (stem2) targets.add(stem2);
7745
+ return [...targets];
7746
+ }
7747
+
7655
7748
  // src/core/shared/hubExport.ts
7656
7749
  var EIGEN_ITERATIONS = 50;
7657
7750
  function computeHubScores(index) {
@@ -7735,15 +7828,16 @@ function computeHubScores(index) {
7735
7828
  }
7736
7829
  return { scores: result, edgeCount };
7737
7830
  }
7738
- function updateHubScoresInDb(stateDb2, hubScores) {
7739
- const updateStmt = stateDb2.db.prepare(`
7740
- UPDATE entities SET hub_score = ? WHERE name_lower = ?
7741
- `);
7831
+ function updateHubScoresInDb(stateDb2, hubScores, pathToId) {
7832
+ const resetAll = stateDb2.db.prepare("UPDATE entities SET hub_score = 0");
7833
+ const updateById = stateDb2.db.prepare("UPDATE entities SET hub_score = ? WHERE id = ?");
7742
7834
  let updated = 0;
7743
7835
  const transaction = stateDb2.db.transaction(() => {
7744
- for (const [nameLower, score] of hubScores) {
7745
- const result = updateStmt.run(score, nameLower);
7746
- if (result.changes > 0) {
7836
+ resetAll.run();
7837
+ for (const [normalizedPath, score] of hubScores) {
7838
+ const entityId = pathToId.get(normalizedPath);
7839
+ if (entityId !== void 0) {
7840
+ updateById.run(score, entityId);
7747
7841
  updated++;
7748
7842
  }
7749
7843
  }
@@ -7758,9 +7852,14 @@ async function exportHubScores(vaultIndex2, stateDb2) {
7758
7852
  }
7759
7853
  const { scores: hubScores, edgeCount } = computeHubScores(vaultIndex2);
7760
7854
  console.error(`[Flywheel] Computed hub scores for ${hubScores.size} notes (${edgeCount} edges in graph)`);
7855
+ const entityRows = stateDb2.db.prepare("SELECT id, path FROM entities").all();
7856
+ const pathToId = /* @__PURE__ */ new Map();
7857
+ for (const row of entityRows) {
7858
+ pathToId.set(normalizeResolvedPath(row.path), row.id);
7859
+ }
7761
7860
  try {
7762
- const updated = updateHubScoresInDb(stateDb2, hubScores);
7763
- console.error(`[Flywheel] Updated ${updated} hub scores in StateDb`);
7861
+ const updated = updateHubScoresInDb(stateDb2, hubScores, pathToId);
7862
+ console.error(`[Flywheel] Hub scores: ${updated}/${hubScores.size} scores mapped to entities (all others reset to 0)`);
7764
7863
  return updated;
7765
7864
  } catch (e) {
7766
7865
  console.error("[Flywheel] Failed to update hub scores in StateDb:", e);
@@ -8703,8 +8802,14 @@ var PipelineRunner = class {
8703
8802
  } catch {
8704
8803
  }
8705
8804
  }
8706
- tracker.end({ updated: embUpdated, removed: embRemoved });
8707
- serverLog("watcher", `Note embeddings: ${embUpdated} updated, ${embRemoved} removed`);
8805
+ let orphansRemoved = 0;
8806
+ try {
8807
+ orphansRemoved = removeOrphanedNoteEmbeddings();
8808
+ } catch (e) {
8809
+ serverLog("watcher", `Note embedding orphan cleanup failed: ${e}`, "error");
8810
+ }
8811
+ tracker.end({ updated: embUpdated, removed: embRemoved, orphans_removed: orphansRemoved });
8812
+ serverLog("watcher", `Note embeddings: ${embUpdated} updated, ${embRemoved} removed, ${orphansRemoved} orphans cleaned`);
8708
8813
  } else {
8709
8814
  tracker.skip("note_embeddings", "not built");
8710
8815
  }
@@ -8715,6 +8820,7 @@ var PipelineRunner = class {
8715
8820
  if (hasEntityEmbeddingsIndex() && p.sd) {
8716
8821
  tracker.start("entity_embeddings", { files: p.events.length });
8717
8822
  let entEmbUpdated = 0;
8823
+ let entEmbOrphansRemoved = 0;
8718
8824
  const entEmbNames = [];
8719
8825
  try {
8720
8826
  const allEntities = getAllEntitiesFromDb(p.sd);
@@ -8732,10 +8838,13 @@ var PipelineRunner = class {
8732
8838
  entEmbNames.push(entity.name);
8733
8839
  }
8734
8840
  }
8735
- } catch {
8841
+ const currentNames = new Set(allEntities.map((e) => e.name));
8842
+ entEmbOrphansRemoved = removeOrphanedEntityEmbeddings(currentNames);
8843
+ } catch (e) {
8844
+ serverLog("watcher", `Entity embedding update/orphan cleanup failed: ${e}`, "error");
8736
8845
  }
8737
- tracker.end({ updated: entEmbUpdated, updated_entities: entEmbNames.slice(0, 10) });
8738
- serverLog("watcher", `Entity embeddings: ${entEmbUpdated} updated`);
8846
+ tracker.end({ updated: entEmbUpdated, updated_entities: entEmbNames.slice(0, 10), orphans_removed: entEmbOrphansRemoved });
8847
+ serverLog("watcher", `Entity embeddings: ${entEmbUpdated} updated, ${entEmbOrphansRemoved} orphans cleaned`);
8739
8848
  } else {
8740
8849
  tracker.skip("entity_embeddings", !p.sd ? "no sd" : "not built");
8741
8850
  }
@@ -9283,9 +9392,15 @@ var PipelineRunner = class {
9283
9392
  return { skipped: true, reason: "vacuumed recently" };
9284
9393
  }
9285
9394
  p.sd.db.pragma("incremental_vacuum");
9286
- p.sd.setMetadataValue.run("last_incremental_vacuum", String(Date.now()));
9287
- serverLog("watcher", "Incremental vacuum completed");
9288
- return { vacuumed: true };
9395
+ const walResult = p.sd.db.pragma("wal_checkpoint(TRUNCATE)");
9396
+ const checkpointed = walResult?.[0]?.busy === 0;
9397
+ if (checkpointed) {
9398
+ p.sd.setMetadataValue.run("last_incremental_vacuum", String(Date.now()));
9399
+ serverLog("watcher", "Incremental vacuum + WAL checkpoint completed");
9400
+ } else {
9401
+ serverLog("watcher", "Incremental vacuum done, WAL checkpoint skipped (busy readers)");
9402
+ }
9403
+ return { vacuumed: true, wal_checkpointed: checkpointed };
9289
9404
  }
9290
9405
  };
9291
9406
 
@@ -9788,11 +9903,9 @@ function computeMetrics(index, stateDb2) {
9788
9903
  for (const bl of backlinks) {
9789
9904
  connectedNotes.add(bl.source);
9790
9905
  }
9791
- for (const note of index.notes.values()) {
9792
- const normalizedTitle = note.title.toLowerCase();
9793
- if (normalizedTitle === target.toLowerCase() || note.path.toLowerCase() === target.toLowerCase()) {
9794
- connectedNotes.add(note.path);
9795
- }
9906
+ const targetPath = index.entities.get(target);
9907
+ if (targetPath && index.notes.has(targetPath)) {
9908
+ connectedNotes.add(targetPath);
9796
9909
  }
9797
9910
  }
9798
9911
  let orphanCount = 0;
@@ -10824,14 +10937,18 @@ function rankOutlinks(outlinks, notePath, index, stateDb2, maxLinks = TOP_LINKS)
10824
10937
  }).sort((a, b) => (b.weight ?? 1) - (a.weight ?? 1)).slice(0, maxLinks);
10825
10938
  }
10826
10939
  function rankBacklinks(backlinks, notePath, index, stateDb2, maxLinks = TOP_LINKS) {
10827
- const stem2 = notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() ?? "";
10940
+ const targets = getInboundTargetsForNote(stateDb2, notePath);
10828
10941
  const weightMap = /* @__PURE__ */ new Map();
10829
- if (stateDb2 && stem2) {
10942
+ if (stateDb2 && targets.length > 0) {
10830
10943
  try {
10944
+ const placeholders = targets.map(() => "?").join(",");
10831
10945
  const rows = stateDb2.db.prepare(
10832
- "SELECT note_path, weight FROM note_links WHERE target = ?"
10833
- ).all(stem2);
10834
- for (const row of rows) weightMap.set(row.note_path, row.weight);
10946
+ `SELECT note_path, weight FROM note_links WHERE target IN (${placeholders})`
10947
+ ).all(...targets);
10948
+ for (const row of rows) {
10949
+ const existing = weightMap.get(row.note_path) ?? 0;
10950
+ weightMap.set(row.note_path, Math.max(existing, row.weight));
10951
+ }
10835
10952
  } catch {
10836
10953
  }
10837
10954
  }
@@ -12703,16 +12820,18 @@ function registerGraphTools(server2, getIndex, getVaultPath, getStateDb3) {
12703
12820
  return { content: [{ type: "text", text: JSON.stringify({ error: "StateDb not initialized" }) }] };
12704
12821
  }
12705
12822
  const limit = Math.min(requestedLimit ?? 20, MAX_LIMIT);
12706
- const stem2 = notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() ?? "";
12823
+ const targets = getInboundTargetsForNote(stateDb2, notePath);
12824
+ const inPlaceholders = targets.map(() => "?").join(",");
12707
12825
  const rows = stateDb2.db.prepare(`
12708
12826
  SELECT target AS node, weight, 'outgoing' AS direction
12709
12827
  FROM note_links WHERE note_path = ?
12710
12828
  UNION ALL
12711
- SELECT note_path AS node, weight, 'incoming' AS direction
12712
- FROM note_links WHERE target = ?
12829
+ SELECT note_path AS node, MAX(weight) AS weight, 'incoming' AS direction
12830
+ FROM note_links WHERE target IN (${inPlaceholders})
12831
+ GROUP BY note_path
12713
12832
  ORDER BY weight DESC
12714
12833
  LIMIT ?
12715
- `).all(notePath, stem2, limit);
12834
+ `).all(notePath, ...targets, limit);
12716
12835
  return {
12717
12836
  content: [{
12718
12837
  type: "text",
@@ -22920,6 +23039,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb3) {
22920
23039
  if (!force) {
22921
23040
  const diagnosis2 = diagnoseEmbeddings(vaultPath2);
22922
23041
  if (diagnosis2.healthy) {
23042
+ setEmbeddingsBuildState("complete");
22923
23043
  return {
22924
23044
  content: [{
22925
23045
  type: "text",
@@ -24932,7 +25052,8 @@ function registerAllTools(targetServer, ctx) {
24932
25052
  var __filename = fileURLToPath2(import.meta.url);
24933
25053
  var __dirname = dirname7(__filename);
24934
25054
  var pkg = JSON.parse(readFileSync6(join21(__dirname, "../package.json"), "utf-8"));
24935
- var vaultPath = process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
25055
+ var _earlyVaultConfigs = parseVaultConfig();
25056
+ var vaultPath = _earlyVaultConfigs ? _earlyVaultConfigs[0].path : process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
24936
25057
  var resolvedVaultPath;
24937
25058
  try {
24938
25059
  resolvedVaultPath = realpathSync(vaultPath).replace(/\\/g, "/");
@@ -25727,6 +25848,19 @@ if (process.argv.includes("--init-semantic")) {
25727
25848
  process.exit(1);
25728
25849
  });
25729
25850
  }
25851
+ function gracefulShutdown(signal) {
25852
+ console.error(`[Memory] Received ${signal}, shutting down...`);
25853
+ try {
25854
+ watcherInstance?.stop();
25855
+ } catch {
25856
+ }
25857
+ stopSweepTimer();
25858
+ flushLogs().catch(() => {
25859
+ }).finally(() => process.exit(0));
25860
+ setTimeout(() => process.exit(0), 2e3).unref();
25861
+ }
25862
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
25863
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
25730
25864
  process.on("beforeExit", async () => {
25731
25865
  stopSweepTimer();
25732
25866
  await flushLogs();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.156",
4
- "description": "MCP tools that search, write, and auto-link your Obsidian vault and learn from your edits.",
3
+ "version": "2.0.158",
4
+ "description": "MCP tools that search, write, and auto-link your Obsidian vault \u2014 and learn from your edits.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -54,7 +54,7 @@
54
54
  "dependencies": {
55
55
  "@huggingface/transformers": "^3.8.1",
56
56
  "@modelcontextprotocol/sdk": "^1.25.1",
57
- "@velvetmonkey/vault-core": "^2.0.156",
57
+ "@velvetmonkey/vault-core": "^2.0.158",
58
58
  "better-sqlite3": "^12.0.0",
59
59
  "chokidar": "^4.0.0",
60
60
  "gray-matter": "^4.0.3",