@velvetmonkey/flywheel-memory 2.0.54 → 2.0.56

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 +127 -8
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -3832,9 +3832,14 @@ function trackWikilinkApplications(stateDb2, notePath, entities) {
3832
3832
  applied_at = datetime('now'),
3833
3833
  status = 'applied'
3834
3834
  `);
3835
+ const lookupCanonical = stateDb2.db.prepare(
3836
+ `SELECT name FROM entities WHERE LOWER(name) = LOWER(?) LIMIT 1`
3837
+ );
3835
3838
  const transaction = stateDb2.db.transaction(() => {
3836
3839
  for (const entity of entities) {
3837
- upsert.run(entity, notePath);
3840
+ const row = lookupCanonical.get(entity);
3841
+ const canonicalName = row?.name ?? entity;
3842
+ upsert.run(canonicalName, notePath);
3838
3843
  }
3839
3844
  });
3840
3845
  transaction();
@@ -5496,13 +5501,13 @@ var EXCLUDED_FOLDERS2 = /* @__PURE__ */ new Set([
5496
5501
  ".claude",
5497
5502
  ".git"
5498
5503
  ]);
5499
- function noteContainsEntity(content, entityName) {
5504
+ function noteContainsEntity(content, entityName, contentTokens) {
5500
5505
  const entityTokens = tokenize(entityName);
5501
5506
  if (entityTokens.length === 0) return false;
5502
- const contentTokens = new Set(tokenize(content));
5507
+ const tokens = contentTokens ?? new Set(tokenize(content));
5503
5508
  let matchCount = 0;
5504
5509
  for (const token of entityTokens) {
5505
- if (contentTokens.has(token)) {
5510
+ if (tokens.has(token)) {
5506
5511
  matchCount++;
5507
5512
  }
5508
5513
  }
@@ -5547,9 +5552,10 @@ async function mineCooccurrences(vaultPath2, entities, options = {}) {
5547
5552
  try {
5548
5553
  const content = await readFile2(file.path, "utf-8");
5549
5554
  notesScanned++;
5555
+ const contentTokens = new Set(tokenize(content));
5550
5556
  const mentionedEntities = [];
5551
5557
  for (const entity of validEntities) {
5552
- if (noteContainsEntity(content, entity)) {
5558
+ if (noteContainsEntity(content, entity, contentTokens)) {
5553
5559
  mentionedEntities.push(entity);
5554
5560
  }
5555
5561
  }
@@ -8886,6 +8892,20 @@ function purgeOldIndexEvents(stateDb2, retentionDays = 90) {
8886
8892
  ).run(cutoff);
8887
8893
  return result.changes;
8888
8894
  }
8895
+ function purgeOldSuggestionEvents(stateDb2, retentionDays = 30) {
8896
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
8897
+ const result = stateDb2.db.prepare(
8898
+ "DELETE FROM suggestion_events WHERE timestamp < ?"
8899
+ ).run(cutoff);
8900
+ return result.changes;
8901
+ }
8902
+ function purgeOldNoteLinkHistory(stateDb2, retentionDays = 90) {
8903
+ const cutoff = new Date(Date.now() - retentionDays * 24 * 60 * 60 * 1e3).toISOString();
8904
+ const result = stateDb2.db.prepare(
8905
+ "DELETE FROM note_link_history WHERE last_positive_at < ?"
8906
+ ).run(cutoff);
8907
+ return result.changes;
8908
+ }
8889
8909
 
8890
8910
  // src/core/read/sweep.ts
8891
8911
  var DEFAULT_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
@@ -8944,13 +8964,20 @@ function runSweep(index) {
8944
8964
  cachedResults = results;
8945
8965
  return results;
8946
8966
  }
8947
- function startSweepTimer(getIndex, intervalMs) {
8967
+ function startSweepTimer(getIndex, intervalMs, maintenanceCallback) {
8948
8968
  const interval = Math.max(intervalMs ?? DEFAULT_SWEEP_INTERVAL_MS, MIN_SWEEP_INTERVAL_MS);
8949
8969
  setTimeout(() => {
8950
8970
  doSweep(getIndex);
8951
8971
  }, 5e3);
8952
8972
  sweepTimer = setInterval(() => {
8953
8973
  doSweep(getIndex);
8974
+ if (maintenanceCallback) {
8975
+ try {
8976
+ maintenanceCallback();
8977
+ } catch (err) {
8978
+ console.error("[Flywheel] Maintenance error:", err);
8979
+ }
8980
+ }
8954
8981
  }, interval);
8955
8982
  if (sweepTimer && typeof sweepTimer === "object" && "unref" in sweepTimer) {
8956
8983
  sweepTimer.unref();
@@ -8970,6 +8997,12 @@ function doSweep(getIndex) {
8970
8997
  sweepRunning = false;
8971
8998
  }
8972
8999
  }
9000
+ function stopSweepTimer() {
9001
+ if (sweepTimer) {
9002
+ clearInterval(sweepTimer);
9003
+ sweepTimer = null;
9004
+ }
9005
+ }
8973
9006
  function getSweepResults() {
8974
9007
  return cachedResults;
8975
9008
  }
@@ -16750,6 +16783,60 @@ function getRecentSessionSummaries(stateDb2, limit = 5, agent_id) {
16750
16783
  "SELECT * FROM session_summaries ORDER BY ended_at DESC LIMIT ?"
16751
16784
  ).all(limit);
16752
16785
  }
16786
+ function sweepExpiredMemories(stateDb2) {
16787
+ const now = Date.now();
16788
+ const msPerDay = 864e5;
16789
+ const expired = stateDb2.db.prepare(`
16790
+ SELECT key FROM memories
16791
+ WHERE ttl_days IS NOT NULL
16792
+ AND superseded_by IS NULL
16793
+ AND (created_at + (ttl_days * ?)) < ?
16794
+ `).all(msPerDay, now);
16795
+ for (const { key } of expired) {
16796
+ removeGraphSignals(stateDb2, key);
16797
+ }
16798
+ const result = stateDb2.db.prepare(`
16799
+ DELETE FROM memories
16800
+ WHERE ttl_days IS NOT NULL
16801
+ AND superseded_by IS NULL
16802
+ AND (created_at + (ttl_days * ?)) < ?
16803
+ `).run(msPerDay, now);
16804
+ return result.changes;
16805
+ }
16806
+ function decayMemoryConfidence(stateDb2) {
16807
+ const now = Date.now();
16808
+ const msPerDay = 864e5;
16809
+ const halfLifeDays = 30;
16810
+ const lambda = Math.LN2 / (halfLifeDays * msPerDay);
16811
+ const staleThreshold = now - 7 * msPerDay;
16812
+ const staleMemories = stateDb2.db.prepare(`
16813
+ SELECT id, accessed_at, confidence FROM memories
16814
+ WHERE accessed_at < ? AND superseded_by IS NULL AND confidence > 0.1
16815
+ `).all(staleThreshold);
16816
+ let updated = 0;
16817
+ const updateStmt = stateDb2.db.prepare(
16818
+ "UPDATE memories SET confidence = ? WHERE id = ?"
16819
+ );
16820
+ for (const mem of staleMemories) {
16821
+ const ageDays = (now - mem.accessed_at) / msPerDay;
16822
+ const decayFactor = Math.exp(-lambda * ageDays * msPerDay);
16823
+ const newConfidence = Math.max(0.1, mem.confidence * decayFactor);
16824
+ if (Math.abs(newConfidence - mem.confidence) > 0.01) {
16825
+ updateStmt.run(newConfidence, mem.id);
16826
+ updated++;
16827
+ }
16828
+ }
16829
+ return updated;
16830
+ }
16831
+ function pruneSupersededMemories(stateDb2, retentionDays = 90) {
16832
+ const cutoff = Date.now() - retentionDays * 864e5;
16833
+ const result = stateDb2.db.prepare(`
16834
+ DELETE FROM memories
16835
+ WHERE superseded_by IS NOT NULL
16836
+ AND updated_at < ?
16837
+ `).run(cutoff);
16838
+ return result.changes;
16839
+ }
16753
16840
  function findContradictions2(stateDb2, entity) {
16754
16841
  const conditions = ["superseded_by IS NULL"];
16755
16842
  const params = [];
@@ -19218,6 +19305,23 @@ async function buildStartupCatchupBatch(vaultPath2, sinceMs) {
19218
19305
  await scanDir(vaultPath2);
19219
19306
  return events;
19220
19307
  }
19308
+ var lastPurgeAt = Date.now();
19309
+ function runPeriodicMaintenance(db4) {
19310
+ sweepExpiredMemories(db4);
19311
+ decayMemoryConfidence(db4);
19312
+ pruneSupersededMemories(db4, 90);
19313
+ const now = Date.now();
19314
+ if (now - lastPurgeAt > 24 * 60 * 60 * 1e3) {
19315
+ purgeOldMetrics(db4, 90);
19316
+ purgeOldIndexEvents(db4, 90);
19317
+ purgeOldInvocations(db4, 90);
19318
+ purgeOldSuggestionEvents(db4, 30);
19319
+ purgeOldNoteLinkHistory(db4, 90);
19320
+ purgeOldSnapshots(db4, 90);
19321
+ lastPurgeAt = now;
19322
+ serverLog("server", "Daily purge complete");
19323
+ }
19324
+ }
19221
19325
  async function runPostIndexWork(index) {
19222
19326
  const postStart = Date.now();
19223
19327
  serverLog("index", "Scanning entities...");
@@ -19233,6 +19337,11 @@ async function runPostIndexWork(index) {
19233
19337
  purgeOldMetrics(stateDb, 90);
19234
19338
  purgeOldIndexEvents(stateDb, 90);
19235
19339
  purgeOldInvocations(stateDb, 90);
19340
+ purgeOldSuggestionEvents(stateDb, 30);
19341
+ purgeOldNoteLinkHistory(stateDb, 90);
19342
+ sweepExpiredMemories(stateDb);
19343
+ decayMemoryConfidence(stateDb);
19344
+ pruneSupersededMemories(stateDb, 90);
19236
19345
  serverLog("server", "Growth metrics recorded");
19237
19346
  } catch (err) {
19238
19347
  serverLog("server", `Failed to record metrics: ${err instanceof Error ? err.message : err}`, "error");
@@ -19827,6 +19936,9 @@ async function runPostIndexWork(index) {
19827
19936
  tracker.end({ tracked: trackedLinks, mentions: mentionResults });
19828
19937
  serverLog("watcher", `Wikilink check: ${trackedLinks.reduce((s, t) => s + t.entities.length, 0)} tracked links in ${trackedLinks.length} files, ${mentionResults.reduce((s, m) => s + m.entities.length, 0)} unwikified mentions`);
19829
19938
  tracker.start("implicit_feedback", { files: filteredEvents.length });
19939
+ const deletedFiles = new Set(
19940
+ filteredEvents.filter((e) => e.type === "delete").map((e) => e.path)
19941
+ );
19830
19942
  const preSuppressed = stateDb ? new Set(getAllSuppressionPenalties(stateDb).keys()) : /* @__PURE__ */ new Set();
19831
19943
  const feedbackResults = [];
19832
19944
  if (stateDb) {
@@ -19842,6 +19954,7 @@ async function runPostIndexWork(index) {
19842
19954
  }
19843
19955
  if (stateDb && linkDiffs.length > 0) {
19844
19956
  for (const diff of linkDiffs) {
19957
+ if (deletedFiles.has(diff.file)) continue;
19845
19958
  for (const target of diff.removed) {
19846
19959
  if (feedbackResults.some((r) => r.entity === target && r.file === diff.file)) continue;
19847
19960
  const entity = entitiesAfter.find(
@@ -19860,6 +19973,7 @@ async function runPostIndexWork(index) {
19860
19973
  `SELECT 1 FROM wikilink_applications WHERE LOWER(entity) = LOWER(?) AND note_path = ? AND status = 'applied'`
19861
19974
  );
19862
19975
  for (const diff of linkDiffs) {
19976
+ if (deletedFiles.has(diff.file)) continue;
19863
19977
  for (const target of diff.added) {
19864
19978
  if (checkApplication.get(target, diff.file)) continue;
19865
19979
  const entity = entitiesAfter.find(
@@ -20071,8 +20185,12 @@ async function runPostIndexWork(index) {
20071
20185
  watcher.start();
20072
20186
  serverLog("watcher", "File watcher started");
20073
20187
  }
20074
- startSweepTimer(() => vaultIndex);
20075
- serverLog("server", "Sweep timer started (5 min interval)");
20188
+ if (process.env.FLYWHEEL_WATCH !== "false") {
20189
+ startSweepTimer(() => vaultIndex, void 0, () => {
20190
+ if (stateDb) runPeriodicMaintenance(stateDb);
20191
+ });
20192
+ serverLog("server", "Sweep timer started (5 min interval)");
20193
+ }
20076
20194
  const postDuration = Date.now() - postStart;
20077
20195
  serverLog("server", `Post-index work complete in ${postDuration}ms`);
20078
20196
  }
@@ -20103,6 +20221,7 @@ if (process.argv.includes("--init-semantic")) {
20103
20221
  });
20104
20222
  }
20105
20223
  process.on("beforeExit", async () => {
20224
+ stopSweepTimer();
20106
20225
  await flushLogs();
20107
20226
  });
20108
20227
  export {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.54",
3
+ "version": "2.0.56",
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.54",
55
+ "@velvetmonkey/vault-core": "^2.0.55",
56
56
  "better-sqlite3": "^11.0.0",
57
57
  "chokidar": "^4.0.0",
58
58
  "gray-matter": "^4.0.3",