@velvetmonkey/flywheel-memory 2.0.43 → 2.0.44

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 +96 -16
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -3627,9 +3627,12 @@ function getSuppressedCount(stateDb2) {
3627
3627
  return row.count;
3628
3628
  }
3629
3629
  function getSuppressedEntities(stateDb2) {
3630
- return stateDb2.db.prepare(
3631
- "SELECT entity, false_positive_rate FROM wikilink_suppressions ORDER BY false_positive_rate DESC"
3632
- ).all();
3630
+ return stateDb2.db.prepare(`
3631
+ SELECT s.entity, s.false_positive_rate,
3632
+ COALESCE((SELECT COUNT(*) FROM wikilink_feedback WHERE entity = s.entity), 0) as total
3633
+ FROM wikilink_suppressions s
3634
+ ORDER BY s.false_positive_rate DESC
3635
+ `).all();
3633
3636
  }
3634
3637
  function computeBoostFromAccuracy(accuracy, sampleCount) {
3635
3638
  if (sampleCount < FEEDBACK_BOOST_MIN_SAMPLES) return 0;
@@ -3784,6 +3787,9 @@ function processImplicitFeedback(stateDb2, notePath, currentContent) {
3784
3787
  }
3785
3788
  });
3786
3789
  transaction();
3790
+ if (removed.length > 0) {
3791
+ updateSuppressionList(stateDb2);
3792
+ }
3787
3793
  return removed;
3788
3794
  }
3789
3795
  var TIER_LABELS = [
@@ -5407,7 +5413,7 @@ function recomputeEdgeWeights(stateDb2) {
5407
5413
  "SELECT note_path, target FROM note_links"
5408
5414
  ).all();
5409
5415
  if (edges.length === 0) {
5410
- return { edges_updated: 0, duration_ms: Date.now() - start, total_weighted: 0, avg_weight: 0, strong_count: 0 };
5416
+ return { edges_updated: 0, duration_ms: Date.now() - start, total_weighted: 0, avg_weight: 0, strong_count: 0, top_changes: [] };
5411
5417
  }
5412
5418
  const survivalMap = /* @__PURE__ */ new Map();
5413
5419
  const historyRows = stateDb2.db.prepare(
@@ -5471,10 +5477,18 @@ function recomputeEdgeWeights(stateDb2) {
5471
5477
  }
5472
5478
  }
5473
5479
  }
5480
+ const oldWeights = /* @__PURE__ */ new Map();
5481
+ const oldRows = stateDb2.db.prepare(
5482
+ "SELECT note_path, target, weight FROM note_links"
5483
+ ).all();
5484
+ for (const row of oldRows) {
5485
+ oldWeights.set(`${row.note_path}\0${row.target}`, row.weight);
5486
+ }
5474
5487
  const now = Date.now();
5475
5488
  const update = stateDb2.db.prepare(
5476
5489
  "UPDATE note_links SET weight = ?, weight_updated_at = ? WHERE note_path = ? AND target = ?"
5477
5490
  );
5491
+ const changes = [];
5478
5492
  const tx = stateDb2.db.transaction(() => {
5479
5493
  for (const edge of edges) {
5480
5494
  const edgeKey = `${edge.note_path}\0${edge.target}`;
@@ -5482,10 +5496,27 @@ function recomputeEdgeWeights(stateDb2) {
5482
5496
  const coSessions = coSessionCount.get(edgeKey) ?? 0;
5483
5497
  const sourceAccess = sourceActivityCount.get(edge.note_path) ?? 0;
5484
5498
  const weight = 1 + editsSurvived * 0.5 + Math.min(coSessions * 0.5, 3) + Math.min(sourceAccess * 0.2, 2);
5485
- update.run(Math.round(weight * 1e3) / 1e3, now, edge.note_path, edge.target);
5499
+ const roundedWeight = Math.round(weight * 1e3) / 1e3;
5500
+ const oldWeight = oldWeights.get(edgeKey) ?? 1;
5501
+ const delta = roundedWeight - oldWeight;
5502
+ if (Math.abs(delta) >= 1e-3) {
5503
+ changes.push({
5504
+ note_path: edge.note_path,
5505
+ target: edge.target,
5506
+ old_weight: oldWeight,
5507
+ new_weight: roundedWeight,
5508
+ delta: Math.round(delta * 1e3) / 1e3,
5509
+ edits_survived: editsSurvived,
5510
+ co_sessions: coSessions,
5511
+ source_access: sourceAccess
5512
+ });
5513
+ }
5514
+ update.run(roundedWeight, now, edge.note_path, edge.target);
5486
5515
  }
5487
5516
  });
5488
5517
  tx();
5518
+ changes.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
5519
+ const top_changes = changes.slice(0, 10);
5489
5520
  const stats = stateDb2.db.prepare(`
5490
5521
  SELECT
5491
5522
  COUNT(*) as total_weighted,
@@ -5499,7 +5530,8 @@ function recomputeEdgeWeights(stateDb2) {
5499
5530
  duration_ms: Date.now() - start,
5500
5531
  total_weighted: stats?.total_weighted ?? 0,
5501
5532
  avg_weight: Math.round((stats?.avg_weight ?? 0) * 100) / 100,
5502
- strong_count: stats?.strong_count ?? 0
5533
+ strong_count: stats?.strong_count ?? 0,
5534
+ top_changes
5503
5535
  };
5504
5536
  }
5505
5537
  function getEntityEdgeWeightMap(stateDb2) {
@@ -6202,6 +6234,12 @@ async function suggestRelatedLinks(content, options = {}) {
6202
6234
  if (linkedEntities.has(entityName.toLowerCase())) {
6203
6235
  continue;
6204
6236
  }
6237
+ if (moduleStateDb5 && !disabled.has("feedback")) {
6238
+ const noteFolder2 = notePath ? notePath.split("/").slice(0, -1).join("/") : void 0;
6239
+ if (isSuppressed(moduleStateDb5, entityName, noteFolder2)) {
6240
+ continue;
6241
+ }
6242
+ }
6205
6243
  const contentScore = disabled.has("exact_match") && disabled.has("stem_match") ? 0 : scoreEntity(entity, contentTokens, contentStems, config);
6206
6244
  let score = contentScore;
6207
6245
  if (contentScore > 0) {
@@ -6251,6 +6289,10 @@ async function suggestRelatedLinks(content, options = {}) {
6251
6289
  if (!disabled.has("length_filter") && entityName.length > MAX_ENTITY_LENGTH) continue;
6252
6290
  if (!disabled.has("article_filter") && isLikelyArticleTitle(entityName)) continue;
6253
6291
  if (linkedEntities.has(entityName.toLowerCase())) continue;
6292
+ if (moduleStateDb5 && !disabled.has("feedback")) {
6293
+ const noteFolder2 = notePath ? notePath.split("/").slice(0, -1).join("/") : void 0;
6294
+ if (isSuppressed(moduleStateDb5, entityName, noteFolder2)) continue;
6295
+ }
6254
6296
  const boost = getCooccurrenceBoost(entityName, directlyMatchedEntities, cooccurrenceIndex, recencyIndex);
6255
6297
  if (boost > 0) {
6256
6298
  const existing = scoredEntities.find((e) => e.name === entityName);
@@ -6315,6 +6357,10 @@ async function suggestRelatedLinks(content, options = {}) {
6315
6357
  existing.score += boost;
6316
6358
  existing.breakdown.semanticBoost = boost;
6317
6359
  } else if (!linkedEntities.has(match.entityName.toLowerCase())) {
6360
+ if (moduleStateDb5 && !disabled.has("feedback")) {
6361
+ const noteFolder2 = notePath ? notePath.split("/").slice(0, -1).join("/") : void 0;
6362
+ if (isSuppressed(moduleStateDb5, match.entityName, noteFolder2)) continue;
6363
+ }
6318
6364
  const entityWithType = entitiesWithTypes.find(
6319
6365
  (et) => et.entity.name === match.entityName
6320
6366
  );
@@ -6419,7 +6465,9 @@ async function suggestRelatedLinks(content, options = {}) {
6419
6465
  if (topSuggestions.length === 0) {
6420
6466
  return emptyResult;
6421
6467
  }
6422
- const suffix = "\u2192 " + topSuggestions.map((name) => `[[${name}]]`).join(", ");
6468
+ const MIN_SUFFIX_SCORE = 12;
6469
+ const suffixEntries = topEntries.filter((e) => e.score >= MIN_SUFFIX_SCORE);
6470
+ const suffix = suffixEntries.length > 0 ? "\u2192 " + suffixEntries.map((e) => `[[${e.name}]]`).join(", ") : "";
6423
6471
  const result = {
6424
6472
  suggestions: topSuggestions,
6425
6473
  suffix
@@ -12892,7 +12940,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
12892
12940
  wikilinkInfo: wikilinkInfo || "none"
12893
12941
  };
12894
12942
  let suggestInfo;
12895
- if (suggestOutgoingLinks && !skipWikilinks) {
12943
+ if (suggestOutgoingLinks && !skipWikilinks && processedContent.length >= 100) {
12896
12944
  const result = await suggestRelatedLinks(processedContent, { maxSuggestions, notePath });
12897
12945
  if (result.suffix) {
12898
12946
  processedContent = processedContent + " " + result.suffix;
@@ -13011,7 +13059,7 @@ Example: vault_add_to_section({ path: "daily/2026-02-15.md", section: "Log", con
13011
13059
  }
13012
13060
  let workingReplacement = validationResult.content;
13013
13061
  let { content: processedReplacement } = maybeApplyWikilinks(workingReplacement, skipWikilinks, notePath);
13014
- if (suggestOutgoingLinks && !skipWikilinks) {
13062
+ if (suggestOutgoingLinks && !skipWikilinks && processedReplacement.length >= 100) {
13015
13063
  const result = await suggestRelatedLinks(processedReplacement, { maxSuggestions, notePath });
13016
13064
  if (result.suffix) {
13017
13065
  processedReplacement = processedReplacement + " " + result.suffix;
@@ -13392,7 +13440,7 @@ function registerNoteTools(server2, vaultPath2, getIndex) {
13392
13440
  }
13393
13441
  let { content: processedContent, wikilinkInfo } = maybeApplyWikilinks(effectiveContent, skipWikilinks, notePath);
13394
13442
  let suggestInfo;
13395
- if (suggestOutgoingLinks && !skipWikilinks) {
13443
+ if (suggestOutgoingLinks && !skipWikilinks && processedContent.length >= 100) {
13396
13444
  const result = await suggestRelatedLinks(processedContent, { maxSuggestions, notePath });
13397
13445
  if (result.suffix) {
13398
13446
  processedContent = processedContent + " " + result.suffix;
@@ -15814,10 +15862,11 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
15814
15862
  days_back: z21.number().optional().describe("Days to look back (default: 30)"),
15815
15863
  granularity: z21.enum(["day", "week"]).optional().describe("Time bucket granularity for layer_timeseries (default: day)"),
15816
15864
  timestamp_before: z21.number().optional().describe("Earlier timestamp for snapshot_diff"),
15817
- timestamp_after: z21.number().optional().describe("Later timestamp for snapshot_diff")
15865
+ timestamp_after: z21.number().optional().describe("Later timestamp for snapshot_diff"),
15866
+ skip_status_update: z21.boolean().optional().describe("Skip marking application as removed (caller will trigger implicit detection via file edit)")
15818
15867
  }
15819
15868
  },
15820
- async ({ mode, entity, note_path, context, correct, limit, days_back, granularity, timestamp_before, timestamp_after }) => {
15869
+ async ({ mode, entity, note_path, context, correct, limit, days_back, granularity, timestamp_before, timestamp_after, skip_status_update }) => {
15821
15870
  const stateDb2 = getStateDb();
15822
15871
  if (!stateDb2) {
15823
15872
  return {
@@ -15845,6 +15894,11 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
15845
15894
  isError: true
15846
15895
  };
15847
15896
  }
15897
+ if (!correct && note_path && !skip_status_update) {
15898
+ stateDb2.db.prepare(
15899
+ `UPDATE wikilink_applications SET status = 'removed' WHERE entity = ? AND note_path = ? COLLATE NOCASE`
15900
+ ).run(entity, note_path);
15901
+ }
15848
15902
  const suppressionUpdated = updateSuppressionList(stateDb2) > 0;
15849
15903
  result = {
15850
15904
  mode: "report",
@@ -17869,7 +17923,15 @@ async function runPostIndexWork(index) {
17869
17923
  if (edgeWeightAgeMs >= 60 * 60 * 1e3) {
17870
17924
  const result = recomputeEdgeWeights(stateDb);
17871
17925
  lastEdgeWeightRebuildAt = Date.now();
17872
- tracker.end({ rebuilt: true, edges: result.edges_updated, duration_ms: result.duration_ms });
17926
+ tracker.end({
17927
+ rebuilt: true,
17928
+ edges: result.edges_updated,
17929
+ duration_ms: result.duration_ms,
17930
+ total_weighted: result.total_weighted,
17931
+ avg_weight: result.avg_weight,
17932
+ strong_count: result.strong_count,
17933
+ top_changes: result.top_changes
17934
+ });
17873
17935
  serverLog("watcher", `Edge weights: ${result.edges_updated} edges in ${result.duration_ms}ms`);
17874
17936
  } else {
17875
17937
  tracker.end({ rebuilt: false, age_ms: edgeWeightAgeMs });
@@ -18154,9 +18216,27 @@ async function runPostIndexWork(index) {
18154
18216
  }
18155
18217
  }
18156
18218
  }
18157
- tracker.end({ removals: feedbackResults });
18158
- if (feedbackResults.length > 0) {
18159
- serverLog("watcher", `Implicit feedback: ${feedbackResults.length} removals detected`);
18219
+ const additionResults = [];
18220
+ if (stateDb && linkDiffs.length > 0) {
18221
+ const checkApplication = stateDb.db.prepare(
18222
+ `SELECT 1 FROM wikilink_applications WHERE LOWER(entity) = LOWER(?) AND note_path = ? AND status = 'applied'`
18223
+ );
18224
+ for (const diff of linkDiffs) {
18225
+ for (const target of diff.added) {
18226
+ if (checkApplication.get(target, diff.file)) continue;
18227
+ const entity = entitiesAfter.find(
18228
+ (e) => e.nameLower === target || (e.aliases ?? []).some((a) => a.toLowerCase() === target)
18229
+ );
18230
+ if (entity) {
18231
+ recordFeedback(stateDb, entity.name, "implicit:manual_added", diff.file, true);
18232
+ additionResults.push({ entity: entity.name, file: diff.file });
18233
+ }
18234
+ }
18235
+ }
18236
+ }
18237
+ tracker.end({ removals: feedbackResults, additions: additionResults });
18238
+ if (feedbackResults.length > 0 || additionResults.length > 0) {
18239
+ serverLog("watcher", `Implicit feedback: ${feedbackResults.length} removals, ${additionResults.length} manual additions detected`);
18160
18240
  }
18161
18241
  tracker.start("tag_scan", { files: filteredEvents.length });
18162
18242
  const tagDiffs = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.43",
3
+ "version": "2.0.44",
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.43",
55
+ "@velvetmonkey/vault-core": "^2.0.44",
56
56
  "better-sqlite3": "^11.0.0",
57
57
  "chokidar": "^4.0.0",
58
58
  "gray-matter": "^4.0.3",