@velvetmonkey/flywheel-memory 2.0.54 → 2.0.55

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 +89 -5
  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;
@@ -16750,6 +16770,60 @@ function getRecentSessionSummaries(stateDb2, limit = 5, agent_id) {
16750
16770
  "SELECT * FROM session_summaries ORDER BY ended_at DESC LIMIT ?"
16751
16771
  ).all(limit);
16752
16772
  }
16773
+ function sweepExpiredMemories(stateDb2) {
16774
+ const now = Date.now();
16775
+ const msPerDay = 864e5;
16776
+ const expired = stateDb2.db.prepare(`
16777
+ SELECT key FROM memories
16778
+ WHERE ttl_days IS NOT NULL
16779
+ AND superseded_by IS NULL
16780
+ AND (created_at + (ttl_days * ?)) < ?
16781
+ `).all(msPerDay, now);
16782
+ for (const { key } of expired) {
16783
+ removeGraphSignals(stateDb2, key);
16784
+ }
16785
+ const result = stateDb2.db.prepare(`
16786
+ DELETE FROM memories
16787
+ WHERE ttl_days IS NOT NULL
16788
+ AND superseded_by IS NULL
16789
+ AND (created_at + (ttl_days * ?)) < ?
16790
+ `).run(msPerDay, now);
16791
+ return result.changes;
16792
+ }
16793
+ function decayMemoryConfidence(stateDb2) {
16794
+ const now = Date.now();
16795
+ const msPerDay = 864e5;
16796
+ const halfLifeDays = 30;
16797
+ const lambda = Math.LN2 / (halfLifeDays * msPerDay);
16798
+ const staleThreshold = now - 7 * msPerDay;
16799
+ const staleMemories = stateDb2.db.prepare(`
16800
+ SELECT id, accessed_at, confidence FROM memories
16801
+ WHERE accessed_at < ? AND superseded_by IS NULL AND confidence > 0.1
16802
+ `).all(staleThreshold);
16803
+ let updated = 0;
16804
+ const updateStmt = stateDb2.db.prepare(
16805
+ "UPDATE memories SET confidence = ? WHERE id = ?"
16806
+ );
16807
+ for (const mem of staleMemories) {
16808
+ const ageDays = (now - mem.accessed_at) / msPerDay;
16809
+ const decayFactor = Math.exp(-lambda * ageDays * msPerDay);
16810
+ const newConfidence = Math.max(0.1, mem.confidence * decayFactor);
16811
+ if (Math.abs(newConfidence - mem.confidence) > 0.01) {
16812
+ updateStmt.run(newConfidence, mem.id);
16813
+ updated++;
16814
+ }
16815
+ }
16816
+ return updated;
16817
+ }
16818
+ function pruneSupersededMemories(stateDb2, retentionDays = 90) {
16819
+ const cutoff = Date.now() - retentionDays * 864e5;
16820
+ const result = stateDb2.db.prepare(`
16821
+ DELETE FROM memories
16822
+ WHERE superseded_by IS NOT NULL
16823
+ AND updated_at < ?
16824
+ `).run(cutoff);
16825
+ return result.changes;
16826
+ }
16753
16827
  function findContradictions2(stateDb2, entity) {
16754
16828
  const conditions = ["superseded_by IS NULL"];
16755
16829
  const params = [];
@@ -19233,6 +19307,11 @@ async function runPostIndexWork(index) {
19233
19307
  purgeOldMetrics(stateDb, 90);
19234
19308
  purgeOldIndexEvents(stateDb, 90);
19235
19309
  purgeOldInvocations(stateDb, 90);
19310
+ purgeOldSuggestionEvents(stateDb, 30);
19311
+ purgeOldNoteLinkHistory(stateDb, 90);
19312
+ sweepExpiredMemories(stateDb);
19313
+ decayMemoryConfidence(stateDb);
19314
+ pruneSupersededMemories(stateDb, 90);
19236
19315
  serverLog("server", "Growth metrics recorded");
19237
19316
  } catch (err) {
19238
19317
  serverLog("server", `Failed to record metrics: ${err instanceof Error ? err.message : err}`, "error");
@@ -19827,6 +19906,9 @@ async function runPostIndexWork(index) {
19827
19906
  tracker.end({ tracked: trackedLinks, mentions: mentionResults });
19828
19907
  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
19908
  tracker.start("implicit_feedback", { files: filteredEvents.length });
19909
+ const deletedFiles = new Set(
19910
+ filteredEvents.filter((e) => e.type === "delete").map((e) => e.path)
19911
+ );
19830
19912
  const preSuppressed = stateDb ? new Set(getAllSuppressionPenalties(stateDb).keys()) : /* @__PURE__ */ new Set();
19831
19913
  const feedbackResults = [];
19832
19914
  if (stateDb) {
@@ -19842,6 +19924,7 @@ async function runPostIndexWork(index) {
19842
19924
  }
19843
19925
  if (stateDb && linkDiffs.length > 0) {
19844
19926
  for (const diff of linkDiffs) {
19927
+ if (deletedFiles.has(diff.file)) continue;
19845
19928
  for (const target of diff.removed) {
19846
19929
  if (feedbackResults.some((r) => r.entity === target && r.file === diff.file)) continue;
19847
19930
  const entity = entitiesAfter.find(
@@ -19860,6 +19943,7 @@ async function runPostIndexWork(index) {
19860
19943
  `SELECT 1 FROM wikilink_applications WHERE LOWER(entity) = LOWER(?) AND note_path = ? AND status = 'applied'`
19861
19944
  );
19862
19945
  for (const diff of linkDiffs) {
19946
+ if (deletedFiles.has(diff.file)) continue;
19863
19947
  for (const target of diff.added) {
19864
19948
  if (checkApplication.get(target, diff.file)) continue;
19865
19949
  const entity = entitiesAfter.find(
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.55",
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",