@velvetmonkey/flywheel-memory 2.0.157 → 2.1.0

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 +133 -47
  2. package/package.json +2 -2
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;
@@ -665,8 +677,13 @@ function diagnoseEmbeddings(vaultPath2) {
665
677
  } else {
666
678
  checks.push({ name: "entity_orphans", status: "ok", detail: "0 orphaned" });
667
679
  }
668
- } catch {
669
- checks.push({ name: "entity_orphans", status: "ok", detail: "No entity embeddings table" });
680
+ } catch (e) {
681
+ const msg = String(e);
682
+ if (msg.includes("no such table")) {
683
+ checks.push({ name: "entity_orphans", status: "ok", detail: "No entity embeddings table" });
684
+ } else {
685
+ checks.push({ name: "entity_orphans", status: "warning", detail: "Could not check entity orphans" });
686
+ }
670
687
  }
671
688
  try {
672
689
  const samples = db4.prepare("SELECT embedding FROM note_embeddings ORDER BY RANDOM() LIMIT 3").all();
@@ -7701,6 +7718,38 @@ function purgeOldNoteLinkHistory(stateDb2, retentionDays = 90) {
7701
7718
  return result.changes;
7702
7719
  }
7703
7720
 
7721
+ // src/core/read/identity.ts
7722
+ function normalizeResolvedPath(notePath) {
7723
+ return notePath.toLowerCase().replace(/\.md$/, "");
7724
+ }
7725
+ function getInboundTargetsForNote(stateDb2, notePath) {
7726
+ const targets = /* @__PURE__ */ new Set();
7727
+ const stem2 = notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase();
7728
+ const normalizedPath = normalizeResolvedPath(notePath);
7729
+ if (stateDb2) {
7730
+ try {
7731
+ const row = stateDb2.db.prepare(
7732
+ `SELECT name_lower, aliases_json FROM entities
7733
+ WHERE LOWER(REPLACE(path, '.md', '')) = ?`
7734
+ ).get(normalizedPath);
7735
+ if (row) {
7736
+ targets.add(row.name_lower);
7737
+ if (row.aliases_json) {
7738
+ try {
7739
+ for (const alias of JSON.parse(row.aliases_json)) {
7740
+ targets.add(alias.toLowerCase());
7741
+ }
7742
+ } catch {
7743
+ }
7744
+ }
7745
+ }
7746
+ } catch {
7747
+ }
7748
+ }
7749
+ if (stem2) targets.add(stem2);
7750
+ return [...targets];
7751
+ }
7752
+
7704
7753
  // src/core/shared/hubExport.ts
7705
7754
  var EIGEN_ITERATIONS = 50;
7706
7755
  function computeHubScores(index) {
@@ -7784,15 +7833,16 @@ function computeHubScores(index) {
7784
7833
  }
7785
7834
  return { scores: result, edgeCount };
7786
7835
  }
7787
- function updateHubScoresInDb(stateDb2, hubScores) {
7788
- const updateStmt = stateDb2.db.prepare(`
7789
- UPDATE entities SET hub_score = ? WHERE name_lower = ?
7790
- `);
7836
+ function updateHubScoresInDb(stateDb2, hubScores, pathToId) {
7837
+ const resetAll = stateDb2.db.prepare("UPDATE entities SET hub_score = 0");
7838
+ const updateById = stateDb2.db.prepare("UPDATE entities SET hub_score = ? WHERE id = ?");
7791
7839
  let updated = 0;
7792
7840
  const transaction = stateDb2.db.transaction(() => {
7793
- for (const [nameLower, score] of hubScores) {
7794
- const result = updateStmt.run(score, nameLower);
7795
- if (result.changes > 0) {
7841
+ resetAll.run();
7842
+ for (const [normalizedPath, score] of hubScores) {
7843
+ const entityId = pathToId.get(normalizedPath);
7844
+ if (entityId !== void 0) {
7845
+ updateById.run(score, entityId);
7796
7846
  updated++;
7797
7847
  }
7798
7848
  }
@@ -7807,9 +7857,14 @@ async function exportHubScores(vaultIndex2, stateDb2) {
7807
7857
  }
7808
7858
  const { scores: hubScores, edgeCount } = computeHubScores(vaultIndex2);
7809
7859
  console.error(`[Flywheel] Computed hub scores for ${hubScores.size} notes (${edgeCount} edges in graph)`);
7860
+ const entityRows = stateDb2.db.prepare("SELECT id, path FROM entities").all();
7861
+ const pathToId = /* @__PURE__ */ new Map();
7862
+ for (const row of entityRows) {
7863
+ pathToId.set(normalizeResolvedPath(row.path), row.id);
7864
+ }
7810
7865
  try {
7811
- const updated = updateHubScoresInDb(stateDb2, hubScores);
7812
- console.error(`[Flywheel] Updated ${updated} hub scores in StateDb`);
7866
+ const updated = updateHubScoresInDb(stateDb2, hubScores, pathToId);
7867
+ console.error(`[Flywheel] Hub scores: ${updated}/${hubScores.size} scores mapped to entities (all others reset to 0)`);
7813
7868
  return updated;
7814
7869
  } catch (e) {
7815
7870
  console.error("[Flywheel] Failed to update hub scores in StateDb:", e);
@@ -8752,7 +8807,12 @@ var PipelineRunner = class {
8752
8807
  } catch {
8753
8808
  }
8754
8809
  }
8755
- const orphansRemoved = removeOrphanedNoteEmbeddings();
8810
+ let orphansRemoved = 0;
8811
+ try {
8812
+ orphansRemoved = removeOrphanedNoteEmbeddings();
8813
+ } catch (e) {
8814
+ serverLog("watcher", `Note embedding orphan cleanup failed: ${e}`, "error");
8815
+ }
8756
8816
  tracker.end({ updated: embUpdated, removed: embRemoved, orphans_removed: orphansRemoved });
8757
8817
  serverLog("watcher", `Note embeddings: ${embUpdated} updated, ${embRemoved} removed, ${orphansRemoved} orphans cleaned`);
8758
8818
  } else {
@@ -8785,7 +8845,8 @@ var PipelineRunner = class {
8785
8845
  }
8786
8846
  const currentNames = new Set(allEntities.map((e) => e.name));
8787
8847
  entEmbOrphansRemoved = removeOrphanedEntityEmbeddings(currentNames);
8788
- } catch {
8848
+ } catch (e) {
8849
+ serverLog("watcher", `Entity embedding update/orphan cleanup failed: ${e}`, "error");
8789
8850
  }
8790
8851
  tracker.end({ updated: entEmbUpdated, updated_entities: entEmbNames.slice(0, 10), orphans_removed: entEmbOrphansRemoved });
8791
8852
  serverLog("watcher", `Entity embeddings: ${entEmbUpdated} updated, ${entEmbOrphansRemoved} orphans cleaned`);
@@ -9336,10 +9397,15 @@ var PipelineRunner = class {
9336
9397
  return { skipped: true, reason: "vacuumed recently" };
9337
9398
  }
9338
9399
  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 };
9400
+ const walResult = p.sd.db.pragma("wal_checkpoint(TRUNCATE)");
9401
+ const checkpointed = walResult?.[0]?.busy === 0;
9402
+ if (checkpointed) {
9403
+ p.sd.setMetadataValue.run("last_incremental_vacuum", String(Date.now()));
9404
+ serverLog("watcher", "Incremental vacuum + WAL checkpoint completed");
9405
+ } else {
9406
+ serverLog("watcher", "Incremental vacuum done, WAL checkpoint skipped (busy readers)");
9407
+ }
9408
+ return { vacuumed: true, wal_checkpointed: checkpointed };
9343
9409
  }
9344
9410
  };
9345
9411
 
@@ -9842,11 +9908,9 @@ function computeMetrics(index, stateDb2) {
9842
9908
  for (const bl of backlinks) {
9843
9909
  connectedNotes.add(bl.source);
9844
9910
  }
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
- }
9911
+ const targetPath = index.entities.get(target);
9912
+ if (targetPath && index.notes.has(targetPath)) {
9913
+ connectedNotes.add(targetPath);
9850
9914
  }
9851
9915
  }
9852
9916
  let orphanCount = 0;
@@ -10878,14 +10942,18 @@ function rankOutlinks(outlinks, notePath, index, stateDb2, maxLinks = TOP_LINKS)
10878
10942
  }).sort((a, b) => (b.weight ?? 1) - (a.weight ?? 1)).slice(0, maxLinks);
10879
10943
  }
10880
10944
  function rankBacklinks(backlinks, notePath, index, stateDb2, maxLinks = TOP_LINKS) {
10881
- const stem2 = notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() ?? "";
10945
+ const targets = getInboundTargetsForNote(stateDb2, notePath);
10882
10946
  const weightMap = /* @__PURE__ */ new Map();
10883
- if (stateDb2 && stem2) {
10947
+ if (stateDb2 && targets.length > 0) {
10884
10948
  try {
10949
+ const placeholders = targets.map(() => "?").join(",");
10885
10950
  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);
10951
+ `SELECT note_path, weight FROM note_links WHERE target IN (${placeholders})`
10952
+ ).all(...targets);
10953
+ for (const row of rows) {
10954
+ const existing = weightMap.get(row.note_path) ?? 0;
10955
+ weightMap.set(row.note_path, Math.max(existing, row.weight));
10956
+ }
10889
10957
  } catch {
10890
10958
  }
10891
10959
  }
@@ -12757,16 +12825,18 @@ function registerGraphTools(server2, getIndex, getVaultPath, getStateDb3) {
12757
12825
  return { content: [{ type: "text", text: JSON.stringify({ error: "StateDb not initialized" }) }] };
12758
12826
  }
12759
12827
  const limit = Math.min(requestedLimit ?? 20, MAX_LIMIT);
12760
- const stem2 = notePath.replace(/\.md$/, "").split("/").pop()?.toLowerCase() ?? "";
12828
+ const targets = getInboundTargetsForNote(stateDb2, notePath);
12829
+ const inPlaceholders = targets.map(() => "?").join(",");
12761
12830
  const rows = stateDb2.db.prepare(`
12762
12831
  SELECT target AS node, weight, 'outgoing' AS direction
12763
12832
  FROM note_links WHERE note_path = ?
12764
12833
  UNION ALL
12765
- SELECT note_path AS node, weight, 'incoming' AS direction
12766
- FROM note_links WHERE target = ?
12834
+ SELECT note_path AS node, MAX(weight) AS weight, 'incoming' AS direction
12835
+ FROM note_links WHERE target IN (${inPlaceholders})
12836
+ GROUP BY note_path
12767
12837
  ORDER BY weight DESC
12768
12838
  LIMIT ?
12769
- `).all(notePath, stem2, limit);
12839
+ `).all(notePath, ...targets, limit);
12770
12840
  return {
12771
12841
  content: [{
12772
12842
  type: "text",
@@ -22974,6 +23044,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb3) {
22974
23044
  if (!force) {
22975
23045
  const diagnosis2 = diagnoseEmbeddings(vaultPath2);
22976
23046
  if (diagnosis2.healthy) {
23047
+ setEmbeddingsBuildState("complete");
22977
23048
  return {
22978
23049
  content: [{
22979
23050
  type: "text",
@@ -24986,19 +25057,8 @@ function registerAllTools(targetServer, ctx) {
24986
25057
  var __filename = fileURLToPath2(import.meta.url);
24987
25058
  var __dirname = dirname7(__filename);
24988
25059
  var pkg = JSON.parse(readFileSync6(join21(__dirname, "../package.json"), "utf-8"));
24989
- var _earlyVaultConfigs = parseVaultConfig();
24990
- var vaultPath = _earlyVaultConfigs ? _earlyVaultConfigs[0].path : process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
25060
+ var vaultPath;
24991
25061
  var resolvedVaultPath;
24992
- try {
24993
- resolvedVaultPath = realpathSync(vaultPath).replace(/\\/g, "/");
24994
- } catch {
24995
- resolvedVaultPath = vaultPath.replace(/\\/g, "/");
24996
- }
24997
- if (!existsSync3(resolvedVaultPath)) {
24998
- console.error(`[flywheel] Fatal: vault path does not exist: ${resolvedVaultPath}`);
24999
- console.error(`[flywheel] Set PROJECT_PATH or VAULT_PATH to a valid Obsidian vault directory.`);
25000
- process.exit(1);
25001
- }
25002
25062
  var vaultIndex;
25003
25063
  var flywheelConfig = {};
25004
25064
  var stateDb = null;
@@ -25262,10 +25322,21 @@ async function bootVault(ctx, startTime) {
25262
25322
  }
25263
25323
  }
25264
25324
  async function main() {
25325
+ const vaultConfigs = parseVaultConfig();
25326
+ vaultPath = vaultConfigs ? vaultConfigs[0].path : process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
25327
+ try {
25328
+ resolvedVaultPath = realpathSync(vaultPath).replace(/\\/g, "/");
25329
+ } catch {
25330
+ resolvedVaultPath = vaultPath.replace(/\\/g, "/");
25331
+ }
25332
+ if (!existsSync3(resolvedVaultPath)) {
25333
+ console.error(`[flywheel] Fatal: vault path does not exist: ${resolvedVaultPath}`);
25334
+ console.error(`[flywheel] Set PROJECT_PATH or VAULT_PATH to a valid Obsidian vault directory.`);
25335
+ process.exit(1);
25336
+ }
25265
25337
  serverLog("server", `Starting Flywheel Memory v${pkg.version}...`);
25266
25338
  serverLog("server", `Vault: ${vaultPath}`);
25267
25339
  const startTime = Date.now();
25268
- const vaultConfigs = _earlyVaultConfigs;
25269
25340
  if (vaultConfigs) {
25270
25341
  vaultRegistry = new VaultRegistry(vaultConfigs[0].name);
25271
25342
  serverLog("server", `Multi-vault mode: ${vaultConfigs.map((v) => v.name).join(", ")}`);
@@ -25752,17 +25823,19 @@ async function runPostIndexWork(ctx) {
25752
25823
  }
25753
25824
  if (process.argv.includes("--init-semantic")) {
25754
25825
  (async () => {
25826
+ const semanticVaultConfigs = parseVaultConfig();
25827
+ const semanticVaultPath = semanticVaultConfigs ? semanticVaultConfigs[0].path : process.env.PROJECT_PATH || process.env.VAULT_PATH || findVaultRoot();
25755
25828
  console.error("[Semantic] Pre-warming semantic search...");
25756
- console.error(`[Semantic] Vault: ${vaultPath}`);
25829
+ console.error(`[Semantic] Vault: ${semanticVaultPath}`);
25757
25830
  try {
25758
- const db4 = openStateDb(vaultPath);
25831
+ const db4 = openStateDb(semanticVaultPath);
25759
25832
  setEmbeddingsDatabase(db4.db);
25760
25833
  const storedModel = getStoredEmbeddingModel();
25761
25834
  if (storedModel && storedModel !== getActiveModelId()) {
25762
25835
  console.error(`[Semantic] Model changed ${storedModel} \u2192 ${getActiveModelId()}, clearing`);
25763
25836
  clearEmbeddingsForRebuild();
25764
25837
  }
25765
- const progress = await buildEmbeddingsIndex(vaultPath, (p) => {
25838
+ const progress = await buildEmbeddingsIndex(semanticVaultPath, (p) => {
25766
25839
  if (p.current % 50 === 0 || p.current === p.total) {
25767
25840
  console.error(`[Semantic] Embedding ${p.current}/${p.total} notes (${p.skipped} skipped)...`);
25768
25841
  }
@@ -25782,6 +25855,19 @@ if (process.argv.includes("--init-semantic")) {
25782
25855
  process.exit(1);
25783
25856
  });
25784
25857
  }
25858
+ function gracefulShutdown(signal) {
25859
+ console.error(`[Memory] Received ${signal}, shutting down...`);
25860
+ try {
25861
+ watcherInstance?.stop();
25862
+ } catch {
25863
+ }
25864
+ stopSweepTimer();
25865
+ flushLogs().catch(() => {
25866
+ }).finally(() => process.exit(0));
25867
+ setTimeout(() => process.exit(0), 2e3).unref();
25868
+ }
25869
+ process.on("SIGTERM", () => gracefulShutdown("SIGTERM"));
25870
+ process.on("SIGINT", () => gracefulShutdown("SIGINT"));
25785
25871
  process.on("beforeExit", async () => {
25786
25872
  stopSweepTimer();
25787
25873
  await flushLogs();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.157",
3
+ "version": "2.1.0",
4
4
  "description": "MCP tools that search, write, and auto-link your Obsidian vault — and learn from your edits.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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.1.0",
58
58
  "better-sqlite3": "^12.0.0",
59
59
  "chokidar": "^4.0.0",
60
60
  "gray-matter": "^4.0.3",