@velvetmonkey/flywheel-memory 2.0.53 → 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 +171 -61
  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();
@@ -4192,6 +4197,75 @@ function getExtendedDashboardData(stateDb2) {
4192
4197
  };
4193
4198
  }
4194
4199
 
4200
+ // src/core/write/corrections.ts
4201
+ function recordCorrection(stateDb2, type, description, source = "user", entity, notePath) {
4202
+ const result = stateDb2.db.prepare(`
4203
+ INSERT INTO corrections (entity, note_path, correction_type, description, source)
4204
+ VALUES (?, ?, ?, ?, ?)
4205
+ `).run(entity ?? null, notePath ?? null, type, description, source);
4206
+ return stateDb2.db.prepare(
4207
+ "SELECT * FROM corrections WHERE id = ?"
4208
+ ).get(result.lastInsertRowid);
4209
+ }
4210
+ function listCorrections(stateDb2, status, entity, limit = 50) {
4211
+ const conditions = [];
4212
+ const params = [];
4213
+ if (status) {
4214
+ conditions.push("status = ?");
4215
+ params.push(status);
4216
+ }
4217
+ if (entity) {
4218
+ conditions.push("entity = ? COLLATE NOCASE");
4219
+ params.push(entity);
4220
+ }
4221
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
4222
+ params.push(limit);
4223
+ return stateDb2.db.prepare(
4224
+ `SELECT * FROM corrections ${where} ORDER BY created_at DESC LIMIT ?`
4225
+ ).all(...params);
4226
+ }
4227
+ function resolveCorrection(stateDb2, id, newStatus) {
4228
+ const result = stateDb2.db.prepare(`
4229
+ UPDATE corrections
4230
+ SET status = ?, resolved_at = datetime('now')
4231
+ WHERE id = ?
4232
+ `).run(newStatus, id);
4233
+ return result.changes > 0;
4234
+ }
4235
+ function getCorrectedEntityNotePairs(stateDb2) {
4236
+ const rows = stateDb2.db.prepare(`
4237
+ SELECT entity, note_path FROM corrections
4238
+ WHERE correction_type = 'wrong_link'
4239
+ AND entity IS NOT NULL AND note_path IS NOT NULL
4240
+ AND status IN ('pending', 'applied')
4241
+ `).all();
4242
+ const map = /* @__PURE__ */ new Map();
4243
+ for (const row of rows) {
4244
+ const key = row.entity.toLowerCase();
4245
+ if (!map.has(key)) map.set(key, /* @__PURE__ */ new Set());
4246
+ map.get(key).add(row.note_path);
4247
+ }
4248
+ return map;
4249
+ }
4250
+ function processPendingCorrections(stateDb2) {
4251
+ const pending = listCorrections(stateDb2, "pending");
4252
+ let processed = 0;
4253
+ for (const correction of pending) {
4254
+ if (!correction.entity) {
4255
+ resolveCorrection(stateDb2, correction.id, "dismissed");
4256
+ continue;
4257
+ }
4258
+ if (correction.correction_type === "wrong_link") {
4259
+ recordFeedback(stateDb2, correction.entity, "correction:wrong_link", correction.note_path || "", false, 1);
4260
+ } else if (correction.correction_type === "wrong_category") {
4261
+ recordFeedback(stateDb2, correction.entity, "correction:wrong_category", "", false, 0.5);
4262
+ }
4263
+ resolveCorrection(stateDb2, correction.id, "applied");
4264
+ processed++;
4265
+ }
4266
+ return processed;
4267
+ }
4268
+
4195
4269
  // src/core/write/git.ts
4196
4270
  import { simpleGit, CheckRepoActions } from "simple-git";
4197
4271
  import path8 from "path";
@@ -5427,13 +5501,13 @@ var EXCLUDED_FOLDERS2 = /* @__PURE__ */ new Set([
5427
5501
  ".claude",
5428
5502
  ".git"
5429
5503
  ]);
5430
- function noteContainsEntity(content, entityName) {
5504
+ function noteContainsEntity(content, entityName, contentTokens) {
5431
5505
  const entityTokens = tokenize(entityName);
5432
5506
  if (entityTokens.length === 0) return false;
5433
- const contentTokens = new Set(tokenize(content));
5507
+ const tokens = contentTokens ?? new Set(tokenize(content));
5434
5508
  let matchCount = 0;
5435
5509
  for (const token of entityTokens) {
5436
- if (contentTokens.has(token)) {
5510
+ if (tokens.has(token)) {
5437
5511
  matchCount++;
5438
5512
  }
5439
5513
  }
@@ -5478,9 +5552,10 @@ async function mineCooccurrences(vaultPath2, entities, options = {}) {
5478
5552
  try {
5479
5553
  const content = await readFile2(file.path, "utf-8");
5480
5554
  notesScanned++;
5555
+ const contentTokens = new Set(tokenize(content));
5481
5556
  const mentionedEntities = [];
5482
5557
  for (const entity of validEntities) {
5483
- if (noteContainsEntity(content, entity)) {
5558
+ if (noteContainsEntity(content, entity, contentTokens)) {
5484
5559
  mentionedEntities.push(entity);
5485
5560
  }
5486
5561
  }
@@ -5986,6 +6061,14 @@ function processWikilinks(content, notePath, existingContent) {
5986
6061
  return !isSuppressed(moduleStateDb5, name, folder);
5987
6062
  });
5988
6063
  }
6064
+ if (moduleStateDb5 && notePath) {
6065
+ const correctedPairs = getCorrectedEntityNotePairs(moduleStateDb5);
6066
+ entities = entities.filter((e) => {
6067
+ const name = getEntityName2(e).toLowerCase();
6068
+ const paths = correctedPairs.get(name);
6069
+ return !paths || !paths.has(notePath);
6070
+ });
6071
+ }
5989
6072
  const sortedEntities = sortEntitiesByPriority(entities, notePath);
5990
6073
  const resolved = resolveAliasWikilinks(content, sortedEntities, {
5991
6074
  caseInsensitive: true
@@ -6470,6 +6553,7 @@ async function suggestRelatedLinks(content, options = {}) {
6470
6553
  const noteFolder = notePath ? notePath.split("/")[0] : void 0;
6471
6554
  const feedbackBoosts = moduleStateDb5 ? getAllFeedbackBoosts(moduleStateDb5, noteFolder) : /* @__PURE__ */ new Map();
6472
6555
  const suppressionPenalties = moduleStateDb5 ? getAllSuppressionPenalties(moduleStateDb5) : /* @__PURE__ */ new Map();
6556
+ const correctedPairs = moduleStateDb5 ? getCorrectedEntityNotePairs(moduleStateDb5) : /* @__PURE__ */ new Map();
6473
6557
  const edgeWeightMap = moduleStateDb5 ? getEntityEdgeWeightMap(moduleStateDb5) : /* @__PURE__ */ new Map();
6474
6558
  const scoredEntities = [];
6475
6559
  const directlyMatchedEntities = /* @__PURE__ */ new Set();
@@ -6486,6 +6570,10 @@ async function suggestRelatedLinks(content, options = {}) {
6486
6570
  if (linkedEntities.has(entityName.toLowerCase())) {
6487
6571
  continue;
6488
6572
  }
6573
+ if (notePath && correctedPairs.has(entityName.toLowerCase())) {
6574
+ const paths = correctedPairs.get(entityName.toLowerCase());
6575
+ if (paths.has(notePath)) continue;
6576
+ }
6489
6577
  const contentScore = disabled.has("exact_match") && disabled.has("stem_match") ? 0 : scoreEntity(entity, contentTokens, contentStems, config, cooccurrenceIndex);
6490
6578
  let score = contentScore;
6491
6579
  if (contentScore > 0) {
@@ -8804,6 +8892,20 @@ function purgeOldIndexEvents(stateDb2, retentionDays = 90) {
8804
8892
  ).run(cutoff);
8805
8893
  return result.changes;
8806
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
+ }
8807
8909
 
8808
8910
  // src/core/read/sweep.ts
8809
8911
  var DEFAULT_SWEEP_INTERVAL_MS = 5 * 60 * 1e3;
@@ -16352,62 +16454,6 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
16352
16454
 
16353
16455
  // src/tools/write/corrections.ts
16354
16456
  import { z as z22 } from "zod";
16355
-
16356
- // src/core/write/corrections.ts
16357
- function recordCorrection(stateDb2, type, description, source = "user", entity, notePath) {
16358
- const result = stateDb2.db.prepare(`
16359
- INSERT INTO corrections (entity, note_path, correction_type, description, source)
16360
- VALUES (?, ?, ?, ?, ?)
16361
- `).run(entity ?? null, notePath ?? null, type, description, source);
16362
- return stateDb2.db.prepare(
16363
- "SELECT * FROM corrections WHERE id = ?"
16364
- ).get(result.lastInsertRowid);
16365
- }
16366
- function listCorrections(stateDb2, status, entity, limit = 50) {
16367
- const conditions = [];
16368
- const params = [];
16369
- if (status) {
16370
- conditions.push("status = ?");
16371
- params.push(status);
16372
- }
16373
- if (entity) {
16374
- conditions.push("entity = ? COLLATE NOCASE");
16375
- params.push(entity);
16376
- }
16377
- const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
16378
- params.push(limit);
16379
- return stateDb2.db.prepare(
16380
- `SELECT * FROM corrections ${where} ORDER BY created_at DESC LIMIT ?`
16381
- ).all(...params);
16382
- }
16383
- function resolveCorrection(stateDb2, id, newStatus) {
16384
- const result = stateDb2.db.prepare(`
16385
- UPDATE corrections
16386
- SET status = ?, resolved_at = datetime('now')
16387
- WHERE id = ?
16388
- `).run(newStatus, id);
16389
- return result.changes > 0;
16390
- }
16391
- function processPendingCorrections(stateDb2) {
16392
- const pending = listCorrections(stateDb2, "pending");
16393
- let processed = 0;
16394
- for (const correction of pending) {
16395
- if (!correction.entity) {
16396
- resolveCorrection(stateDb2, correction.id, "dismissed");
16397
- continue;
16398
- }
16399
- if (correction.correction_type === "wrong_link") {
16400
- recordFeedback(stateDb2, correction.entity, "correction:wrong_link", correction.note_path || "", false, 1);
16401
- } else if (correction.correction_type === "wrong_category") {
16402
- recordFeedback(stateDb2, correction.entity, "correction:wrong_category", "", false, 0.5);
16403
- }
16404
- resolveCorrection(stateDb2, correction.id, "applied");
16405
- processed++;
16406
- }
16407
- return processed;
16408
- }
16409
-
16410
- // src/tools/write/corrections.ts
16411
16457
  function registerCorrectionTools(server2, getStateDb) {
16412
16458
  server2.tool(
16413
16459
  "vault_record_correction",
@@ -16724,6 +16770,60 @@ function getRecentSessionSummaries(stateDb2, limit = 5, agent_id) {
16724
16770
  "SELECT * FROM session_summaries ORDER BY ended_at DESC LIMIT ?"
16725
16771
  ).all(limit);
16726
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
+ }
16727
16827
  function findContradictions2(stateDb2, entity) {
16728
16828
  const conditions = ["superseded_by IS NULL"];
16729
16829
  const params = [];
@@ -19207,6 +19307,11 @@ async function runPostIndexWork(index) {
19207
19307
  purgeOldMetrics(stateDb, 90);
19208
19308
  purgeOldIndexEvents(stateDb, 90);
19209
19309
  purgeOldInvocations(stateDb, 90);
19310
+ purgeOldSuggestionEvents(stateDb, 30);
19311
+ purgeOldNoteLinkHistory(stateDb, 90);
19312
+ sweepExpiredMemories(stateDb);
19313
+ decayMemoryConfidence(stateDb);
19314
+ pruneSupersededMemories(stateDb, 90);
19210
19315
  serverLog("server", "Growth metrics recorded");
19211
19316
  } catch (err) {
19212
19317
  serverLog("server", `Failed to record metrics: ${err instanceof Error ? err.message : err}`, "error");
@@ -19801,6 +19906,9 @@ async function runPostIndexWork(index) {
19801
19906
  tracker.end({ tracked: trackedLinks, mentions: mentionResults });
19802
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`);
19803
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
+ );
19804
19912
  const preSuppressed = stateDb ? new Set(getAllSuppressionPenalties(stateDb).keys()) : /* @__PURE__ */ new Set();
19805
19913
  const feedbackResults = [];
19806
19914
  if (stateDb) {
@@ -19816,6 +19924,7 @@ async function runPostIndexWork(index) {
19816
19924
  }
19817
19925
  if (stateDb && linkDiffs.length > 0) {
19818
19926
  for (const diff of linkDiffs) {
19927
+ if (deletedFiles.has(diff.file)) continue;
19819
19928
  for (const target of diff.removed) {
19820
19929
  if (feedbackResults.some((r) => r.entity === target && r.file === diff.file)) continue;
19821
19930
  const entity = entitiesAfter.find(
@@ -19834,6 +19943,7 @@ async function runPostIndexWork(index) {
19834
19943
  `SELECT 1 FROM wikilink_applications WHERE LOWER(entity) = LOWER(?) AND note_path = ? AND status = 'applied'`
19835
19944
  );
19836
19945
  for (const diff of linkDiffs) {
19946
+ if (deletedFiles.has(diff.file)) continue;
19837
19947
  for (const target of diff.added) {
19838
19948
  if (checkApplication.get(target, diff.file)) continue;
19839
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.53",
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.53",
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",