@velvetmonkey/flywheel-memory 2.0.157 → 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 +110 -31
  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;
@@ -666,7 +678,7 @@ function diagnoseEmbeddings(vaultPath2) {
666
678
  checks.push({ name: "entity_orphans", status: "ok", detail: "0 orphaned" });
667
679
  }
668
680
  } catch {
669
- checks.push({ name: "entity_orphans", status: "ok", detail: "No entity embeddings table" });
681
+ checks.push({ name: "entity_orphans", status: "warning", detail: "Could not check entity embeddings" });
670
682
  }
671
683
  try {
672
684
  const samples = db4.prepare("SELECT embedding FROM note_embeddings ORDER BY RANDOM() LIMIT 3").all();
@@ -7701,6 +7713,38 @@ function purgeOldNoteLinkHistory(stateDb2, retentionDays = 90) {
7701
7713
  return result.changes;
7702
7714
  }
7703
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
+
7704
7748
  // src/core/shared/hubExport.ts
7705
7749
  var EIGEN_ITERATIONS = 50;
7706
7750
  function computeHubScores(index) {
@@ -7784,15 +7828,16 @@ function computeHubScores(index) {
7784
7828
  }
7785
7829
  return { scores: result, edgeCount };
7786
7830
  }
7787
- function updateHubScoresInDb(stateDb2, hubScores) {
7788
- const updateStmt = stateDb2.db.prepare(`
7789
- UPDATE entities SET hub_score = ? WHERE name_lower = ?
7790
- `);
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 = ?");
7791
7834
  let updated = 0;
7792
7835
  const transaction = stateDb2.db.transaction(() => {
7793
- for (const [nameLower, score] of hubScores) {
7794
- const result = updateStmt.run(score, nameLower);
7795
- 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);
7796
7841
  updated++;
7797
7842
  }
7798
7843
  }
@@ -7807,9 +7852,14 @@ async function exportHubScores(vaultIndex2, stateDb2) {
7807
7852
  }
7808
7853
  const { scores: hubScores, edgeCount } = computeHubScores(vaultIndex2);
7809
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
+ }
7810
7860
  try {
7811
- const updated = updateHubScoresInDb(stateDb2, hubScores);
7812
- 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)`);
7813
7863
  return updated;
7814
7864
  } catch (e) {
7815
7865
  console.error("[Flywheel] Failed to update hub scores in StateDb:", e);
@@ -8752,7 +8802,12 @@ var PipelineRunner = class {
8752
8802
  } catch {
8753
8803
  }
8754
8804
  }
8755
- const orphansRemoved = removeOrphanedNoteEmbeddings();
8805
+ let orphansRemoved = 0;
8806
+ try {
8807
+ orphansRemoved = removeOrphanedNoteEmbeddings();
8808
+ } catch (e) {
8809
+ serverLog("watcher", `Note embedding orphan cleanup failed: ${e}`, "error");
8810
+ }
8756
8811
  tracker.end({ updated: embUpdated, removed: embRemoved, orphans_removed: orphansRemoved });
8757
8812
  serverLog("watcher", `Note embeddings: ${embUpdated} updated, ${embRemoved} removed, ${orphansRemoved} orphans cleaned`);
8758
8813
  } else {
@@ -8785,7 +8840,8 @@ var PipelineRunner = class {
8785
8840
  }
8786
8841
  const currentNames = new Set(allEntities.map((e) => e.name));
8787
8842
  entEmbOrphansRemoved = removeOrphanedEntityEmbeddings(currentNames);
8788
- } catch {
8843
+ } catch (e) {
8844
+ serverLog("watcher", `Entity embedding update/orphan cleanup failed: ${e}`, "error");
8789
8845
  }
8790
8846
  tracker.end({ updated: entEmbUpdated, updated_entities: entEmbNames.slice(0, 10), orphans_removed: entEmbOrphansRemoved });
8791
8847
  serverLog("watcher", `Entity embeddings: ${entEmbUpdated} updated, ${entEmbOrphansRemoved} orphans cleaned`);
@@ -9336,10 +9392,15 @@ var PipelineRunner = class {
9336
9392
  return { skipped: true, reason: "vacuumed recently" };
9337
9393
  }
9338
9394
  p.sd.db.pragma("incremental_vacuum");
9339
- p.sd.db.pragma("wal_checkpoint(TRUNCATE)");
9340
- p.sd.setMetadataValue.run("last_incremental_vacuum", String(Date.now()));
9341
- serverLog("watcher", "Incremental vacuum + WAL checkpoint completed");
9342
- return { vacuumed: true, wal_checkpointed: 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 };
9343
9404
  }
9344
9405
  };
9345
9406
 
@@ -9842,11 +9903,9 @@ function computeMetrics(index, stateDb2) {
9842
9903
  for (const bl of backlinks) {
9843
9904
  connectedNotes.add(bl.source);
9844
9905
  }
9845
- for (const note of index.notes.values()) {
9846
- const normalizedTitle = note.title.toLowerCase();
9847
- if (normalizedTitle === target.toLowerCase() || note.path.toLowerCase() === target.toLowerCase()) {
9848
- connectedNotes.add(note.path);
9849
- }
9906
+ const targetPath = index.entities.get(target);
9907
+ if (targetPath && index.notes.has(targetPath)) {
9908
+ connectedNotes.add(targetPath);
9850
9909
  }
9851
9910
  }
9852
9911
  let orphanCount = 0;
@@ -10878,14 +10937,18 @@ function rankOutlinks(outlinks, notePath, index, stateDb2, maxLinks = TOP_LINKS)
10878
10937
  }).sort((a, b) => (b.weight ?? 1) - (a.weight ?? 1)).slice(0, maxLinks);
10879
10938
  }
10880
10939
  function rankBacklinks(backlinks, notePath, index, stateDb2, maxLinks = TOP_LINKS) {
10881
- const stem2 = notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() ?? "";
10940
+ const targets = getInboundTargetsForNote(stateDb2, notePath);
10882
10941
  const weightMap = /* @__PURE__ */ new Map();
10883
- if (stateDb2 && stem2) {
10942
+ if (stateDb2 && targets.length > 0) {
10884
10943
  try {
10944
+ const placeholders = targets.map(() => "?").join(",");
10885
10945
  const rows = stateDb2.db.prepare(
10886
- "SELECT note_path, weight FROM note_links WHERE target = ?"
10887
- ).all(stem2);
10888
- 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
+ }
10889
10952
  } catch {
10890
10953
  }
10891
10954
  }
@@ -12757,16 +12820,18 @@ function registerGraphTools(server2, getIndex, getVaultPath, getStateDb3) {
12757
12820
  return { content: [{ type: "text", text: JSON.stringify({ error: "StateDb not initialized" }) }] };
12758
12821
  }
12759
12822
  const limit = Math.min(requestedLimit ?? 20, MAX_LIMIT);
12760
- const stem2 = notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() ?? "";
12823
+ const targets = getInboundTargetsForNote(stateDb2, notePath);
12824
+ const inPlaceholders = targets.map(() => "?").join(",");
12761
12825
  const rows = stateDb2.db.prepare(`
12762
12826
  SELECT target AS node, weight, 'outgoing' AS direction
12763
12827
  FROM note_links WHERE note_path = ?
12764
12828
  UNION ALL
12765
- SELECT note_path AS node, weight, 'incoming' AS direction
12766
- 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
12767
12832
  ORDER BY weight DESC
12768
12833
  LIMIT ?
12769
- `).all(notePath, stem2, limit);
12834
+ `).all(notePath, ...targets, limit);
12770
12835
  return {
12771
12836
  content: [{
12772
12837
  type: "text",
@@ -22974,6 +23039,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb3) {
22974
23039
  if (!force) {
22975
23040
  const diagnosis2 = diagnoseEmbeddings(vaultPath2);
22976
23041
  if (diagnosis2.healthy) {
23042
+ setEmbeddingsBuildState("complete");
22977
23043
  return {
22978
23044
  content: [{
22979
23045
  type: "text",
@@ -25265,7 +25331,7 @@ async function main() {
25265
25331
  serverLog("server", `Starting Flywheel Memory v${pkg.version}...`);
25266
25332
  serverLog("server", `Vault: ${vaultPath}`);
25267
25333
  const startTime = Date.now();
25268
- const vaultConfigs = _earlyVaultConfigs;
25334
+ const vaultConfigs = parseVaultConfig();
25269
25335
  if (vaultConfigs) {
25270
25336
  vaultRegistry = new VaultRegistry(vaultConfigs[0].name);
25271
25337
  serverLog("server", `Multi-vault mode: ${vaultConfigs.map((v) => v.name).join(", ")}`);
@@ -25782,6 +25848,19 @@ if (process.argv.includes("--init-semantic")) {
25782
25848
  process.exit(1);
25783
25849
  });
25784
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"));
25785
25864
  process.on("beforeExit", async () => {
25786
25865
  stopSweepTimer();
25787
25866
  await flushLogs();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.157",
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.157",
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",