@velvetmonkey/flywheel-memory 2.0.38 → 2.0.40

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 +55 -6
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -3506,9 +3506,11 @@ var FEEDBACK_BOOST_TIERS = [
3506
3506
  ];
3507
3507
  function recordFeedback(stateDb2, entity, context, notePath, correct) {
3508
3508
  try {
3509
- stateDb2.db.prepare(
3509
+ console.error(`[Flywheel] recordFeedback: entity="${entity}" context="${context}" notePath="${notePath}" correct=${correct}`);
3510
+ const result = stateDb2.db.prepare(
3510
3511
  "INSERT INTO wikilink_feedback (entity, context, note_path, correct) VALUES (?, ?, ?, ?)"
3511
3512
  ).run(entity, context, notePath, correct ? 1 : 0);
3513
+ console.error(`[Flywheel] recordFeedback: inserted id=${result.lastInsertRowid}`);
3512
3514
  } catch (e) {
3513
3515
  console.error(`[Flywheel] recordFeedback failed for entity="${entity}": ${e}`);
3514
3516
  throw e;
@@ -5387,6 +5389,9 @@ function getEffectiveStrictness(notePath) {
5387
5389
  function getCooccurrenceIndex() {
5388
5390
  return cooccurrenceIndex;
5389
5391
  }
5392
+ function setCooccurrenceIndex(index) {
5393
+ cooccurrenceIndex = index;
5394
+ }
5390
5395
  var entityIndex = null;
5391
5396
  var indexReady = false;
5392
5397
  var indexError2 = null;
@@ -5502,6 +5507,11 @@ function checkAndRefreshIfStale() {
5502
5507
  console.error(`[Flywheel] Reloaded ${dbIndex._metadata.total_entities} entities`);
5503
5508
  }
5504
5509
  }
5510
+ const freshRecency = loadRecencyFromStateDb();
5511
+ if (freshRecency && freshRecency.lastUpdated > (recencyIndex?.lastUpdated ?? 0)) {
5512
+ recencyIndex = freshRecency;
5513
+ console.error(`[Flywheel] Refreshed recency index (${freshRecency.lastMentioned.size} entities)`);
5514
+ }
5505
5515
  } catch (e) {
5506
5516
  console.error("[Flywheel] Failed to check for stale entities:", e);
5507
5517
  }
@@ -12164,6 +12174,13 @@ function normalizeInput(content, format) {
12164
12174
  normalized = trimmed;
12165
12175
  changes.push("Trimmed excessive blank lines");
12166
12176
  }
12177
+ const multiLineWikilink = /\[\[([^\]]*\n[^\]]*)\]\]/g;
12178
+ if (multiLineWikilink.test(normalized)) {
12179
+ normalized = normalized.replace(multiLineWikilink, (_match, inner) => {
12180
+ return "[[" + inner.replace(/\s*\n\s*/g, " ").trim() + "]]";
12181
+ });
12182
+ changes.push("Fixed multi-line wikilinks");
12183
+ }
12167
12184
  return {
12168
12185
  content: normalized,
12169
12186
  normalized: changes.length > 0,
@@ -15413,9 +15430,11 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
15413
15430
  let result;
15414
15431
  switch (mode) {
15415
15432
  case "report": {
15433
+ console.error(`[Flywheel] wikilink_feedback report: entity="${entity}" correct=${JSON.stringify(correct)} (type: ${typeof correct})`);
15416
15434
  if (!entity || correct === void 0) {
15417
15435
  return {
15418
- content: [{ type: "text", text: JSON.stringify({ error: "entity and correct are required for report mode" }) }]
15436
+ content: [{ type: "text", text: JSON.stringify({ error: "entity and correct are required for report mode" }) }],
15437
+ isError: true
15419
15438
  };
15420
15439
  }
15421
15440
  try {
@@ -15424,7 +15443,8 @@ function registerWikilinkFeedbackTools(server2, getStateDb) {
15424
15443
  return {
15425
15444
  content: [{ type: "text", text: JSON.stringify({
15426
15445
  error: `Failed to record feedback: ${e instanceof Error ? e.message : String(e)}`
15427
- }) }]
15446
+ }) }],
15447
+ isError: true
15428
15448
  };
15429
15449
  }
15430
15450
  const suppressionUpdated = updateSuppressionList(stateDb2) > 0;
@@ -17083,6 +17103,7 @@ async function main() {
17083
17103
  }
17084
17104
  }
17085
17105
  var DEFAULT_ENTITY_EXCLUDE_FOLDERS = ["node_modules", "templates", "attachments", "tmp"];
17106
+ var lastCooccurrenceRebuildAt = 0;
17086
17107
  async function updateEntitiesInStateDb() {
17087
17108
  if (!stateDb) return;
17088
17109
  try {
@@ -17369,6 +17390,7 @@ async function runPostIndexWork(index) {
17369
17390
  const entitiesAfter = stateDb ? getAllEntitiesFromDb3(stateDb) : [];
17370
17391
  const entityDiff = computeEntityDiff(entitiesBefore, entitiesAfter);
17371
17392
  const categoryChanges = [];
17393
+ const descriptionChanges = [];
17372
17394
  if (stateDb) {
17373
17395
  const beforeMap = new Map(entitiesBefore.map((e) => [e.name, e]));
17374
17396
  const insertChange = stateDb.db.prepare(
@@ -17380,9 +17402,17 @@ async function runPostIndexWork(index) {
17380
17402
  insertChange.run(after.name, "category", before.category, after.category);
17381
17403
  categoryChanges.push({ entity: after.name, from: before.category, to: after.category });
17382
17404
  }
17405
+ if (before) {
17406
+ const oldDesc = before.description ?? null;
17407
+ const newDesc = after.description ?? null;
17408
+ if (oldDesc !== newDesc) {
17409
+ insertChange.run(after.name, "description", oldDesc, newDesc);
17410
+ descriptionChanges.push({ entity: after.name, from: oldDesc, to: newDesc });
17411
+ }
17412
+ }
17383
17413
  }
17384
17414
  }
17385
- tracker.end({ entity_count: entitiesAfter.length, ...entityDiff, category_changes: categoryChanges });
17415
+ tracker.end({ entity_count: entitiesAfter.length, ...entityDiff, category_changes: categoryChanges, description_changes: descriptionChanges });
17386
17416
  serverLog("watcher", `Entity scan: ${entitiesAfter.length} entities`);
17387
17417
  tracker.start("hub_scores", { entity_count: entitiesAfter.length });
17388
17418
  const hubUpdated = await exportHubScores(vaultIndex, stateDb);
@@ -17414,6 +17444,24 @@ async function runPostIndexWork(index) {
17414
17444
  tracker.end({ error: String(e) });
17415
17445
  serverLog("watcher", `Recency: failed: ${e}`);
17416
17446
  }
17447
+ tracker.start("cooccurrence", { entity_count: entitiesAfter.length });
17448
+ try {
17449
+ const cooccurrenceAgeMs = lastCooccurrenceRebuildAt > 0 ? Date.now() - lastCooccurrenceRebuildAt : Infinity;
17450
+ if (cooccurrenceAgeMs >= 60 * 60 * 1e3) {
17451
+ const entityNames = entitiesAfter.map((e) => e.name);
17452
+ const cooccurrenceIdx = await mineCooccurrences(vaultPath, entityNames);
17453
+ setCooccurrenceIndex(cooccurrenceIdx);
17454
+ lastCooccurrenceRebuildAt = Date.now();
17455
+ tracker.end({ rebuilt: true, associations: cooccurrenceIdx._metadata.total_associations });
17456
+ serverLog("watcher", `Co-occurrence: rebuilt ${cooccurrenceIdx._metadata.total_associations} associations`);
17457
+ } else {
17458
+ tracker.end({ rebuilt: false, age_ms: cooccurrenceAgeMs });
17459
+ serverLog("watcher", `Co-occurrence: cache valid (${Math.round(cooccurrenceAgeMs / 1e3)}s old)`);
17460
+ }
17461
+ } catch (e) {
17462
+ tracker.end({ error: String(e) });
17463
+ serverLog("watcher", `Co-occurrence: failed: ${e}`);
17464
+ }
17417
17465
  if (hasEmbeddingsIndex()) {
17418
17466
  tracker.start("note_embeddings", { files: filteredEvents.length });
17419
17467
  let embUpdated = 0;
@@ -17521,6 +17569,7 @@ async function runPostIndexWork(index) {
17521
17569
  }
17522
17570
  }
17523
17571
  const linkDiffs = [];
17572
+ const survivedLinks = [];
17524
17573
  if (stateDb) {
17525
17574
  const upsertHistory = stateDb.db.prepare(`
17526
17575
  INSERT INTO note_link_history (note_path, target) VALUES (?, ?)
@@ -17536,7 +17585,6 @@ async function runPostIndexWork(index) {
17536
17585
  const getEdgeCount = stateDb.db.prepare(
17537
17586
  "SELECT edits_survived FROM note_link_history WHERE note_path=? AND target=?"
17538
17587
  );
17539
- const survivedLinks2 = [];
17540
17588
  for (const entry of forwardLinkResults) {
17541
17589
  const currentSet = /* @__PURE__ */ new Set([
17542
17590
  ...entry.resolved.map((n) => n.toLowerCase()),
@@ -17552,12 +17600,13 @@ async function runPostIndexWork(index) {
17552
17600
  linkDiffs.push({ file: entry.file, ...diff });
17553
17601
  }
17554
17602
  updateStoredNoteLinks(stateDb, entry.file, currentSet);
17603
+ if (diff.removed.length === 0) continue;
17555
17604
  for (const link of currentSet) {
17556
17605
  if (!previousSet.has(link)) continue;
17557
17606
  upsertHistory.run(entry.file, link);
17558
17607
  const countRow = getEdgeCount.get(entry.file, link);
17559
17608
  if (countRow) {
17560
- survivedLinks2.push({ entity: link, file: entry.file, count: countRow.edits_survived });
17609
+ survivedLinks.push({ entity: link, file: entry.file, count: countRow.edits_survived });
17561
17610
  }
17562
17611
  const hit = checkThreshold.get(entry.file, link);
17563
17612
  if (hit) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.0.38",
3
+ "version": "2.0.40",
4
4
  "description": "MCP server that gives Claude full read/write access to your Obsidian vault. 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.38",
55
+ "@velvetmonkey/vault-core": "^2.0.40",
56
56
  "better-sqlite3": "^11.0.0",
57
57
  "chokidar": "^4.0.0",
58
58
  "gray-matter": "^4.0.3",
@@ -74,7 +74,7 @@
74
74
  "engines": {
75
75
  "node": ">=18.0.0"
76
76
  },
77
- "license": "AGPL-3.0-only",
77
+ "license": "Apache-2.0",
78
78
  "files": [
79
79
  "dist",
80
80
  "README.md",