@velvetmonkey/flywheel-memory 2.1.3 → 2.1.4

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 +208 -31
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -7658,6 +7658,18 @@ function computeEntityDiff(before, after) {
7658
7658
  }
7659
7659
  return { added, removed, alias_changes };
7660
7660
  }
7661
+ function getLastSuccessfulEvent(stateDb2) {
7662
+ const row = stateDb2.db.prepare(
7663
+ "SELECT * FROM index_events WHERE success = 1 ORDER BY timestamp DESC LIMIT 1"
7664
+ ).get();
7665
+ return row ? rowToEvent(row) : null;
7666
+ }
7667
+ function getLastEventByTrigger(stateDb2, trigger) {
7668
+ const row = stateDb2.db.prepare(
7669
+ "SELECT * FROM index_events WHERE trigger = ? AND success = 1 ORDER BY timestamp DESC LIMIT 1"
7670
+ ).get(trigger);
7671
+ return row ? rowToEvent(row) : null;
7672
+ }
7661
7673
  function getRecentIndexEvents(stateDb2, limit = 20) {
7662
7674
  const rows = stateDb2.db.prepare(
7663
7675
  "SELECT * FROM index_events ORDER BY timestamp DESC LIMIT ?"
@@ -8515,6 +8527,24 @@ function refreshIfStale(vaultPath2, index, excludeTags) {
8515
8527
  init_wikilinkFeedback();
8516
8528
  init_corrections();
8517
8529
  init_edgeWeights();
8530
+ var PIPELINE_TOTAL_STEPS = 22;
8531
+ var pipelineActivity = {
8532
+ busy: false,
8533
+ trigger: null,
8534
+ started_at: null,
8535
+ current_step: null,
8536
+ completed_steps: 0,
8537
+ total_steps: PIPELINE_TOTAL_STEPS,
8538
+ pending_events: 0,
8539
+ last_completed_at: null,
8540
+ last_completed_trigger: null,
8541
+ last_completed_duration_ms: null,
8542
+ last_completed_files: null,
8543
+ last_completed_steps: []
8544
+ };
8545
+ function getPipelineActivity() {
8546
+ return pipelineActivity;
8547
+ }
8518
8548
  async function runStep(name, tracker, meta, fn) {
8519
8549
  tracker.start(name, meta);
8520
8550
  try {
@@ -8528,7 +8558,22 @@ async function runStep(name, tracker, meta, fn) {
8528
8558
  var PipelineRunner = class {
8529
8559
  constructor(p) {
8530
8560
  this.p = p;
8531
- this.tracker = createStepTracker();
8561
+ const baseTracker = createStepTracker();
8562
+ this.tracker = {
8563
+ steps: baseTracker.steps,
8564
+ start(name, input) {
8565
+ pipelineActivity.current_step = name;
8566
+ baseTracker.start(name, input);
8567
+ },
8568
+ end(output) {
8569
+ baseTracker.end(output);
8570
+ pipelineActivity.completed_steps = baseTracker.steps.length;
8571
+ },
8572
+ skip(name, reason) {
8573
+ baseTracker.skip(name, reason);
8574
+ pipelineActivity.completed_steps = baseTracker.steps.length;
8575
+ }
8576
+ };
8532
8577
  this.batchStart = Date.now();
8533
8578
  }
8534
8579
  tracker;
@@ -8544,6 +8589,13 @@ var PipelineRunner = class {
8544
8589
  suggestionResults = [];
8545
8590
  async run() {
8546
8591
  const { p, tracker } = this;
8592
+ pipelineActivity.busy = true;
8593
+ pipelineActivity.trigger = "watcher";
8594
+ pipelineActivity.started_at = this.batchStart;
8595
+ pipelineActivity.current_step = null;
8596
+ pipelineActivity.completed_steps = 0;
8597
+ pipelineActivity.total_steps = PIPELINE_TOTAL_STEPS;
8598
+ pipelineActivity.pending_events = p.events.length;
8547
8599
  try {
8548
8600
  await runStep("drain_proactive_queue", tracker, {}, () => this.drainQueue());
8549
8601
  await this.indexRebuild();
@@ -8583,6 +8635,13 @@ var PipelineRunner = class {
8583
8635
  steps: tracker.steps
8584
8636
  });
8585
8637
  }
8638
+ pipelineActivity.busy = false;
8639
+ pipelineActivity.current_step = null;
8640
+ pipelineActivity.last_completed_at = Date.now();
8641
+ pipelineActivity.last_completed_trigger = "watcher";
8642
+ pipelineActivity.last_completed_duration_ms = duration;
8643
+ pipelineActivity.last_completed_files = p.events.length;
8644
+ pipelineActivity.last_completed_steps = tracker.steps.map((s) => s.name);
8586
8645
  serverLog("watcher", `Batch complete: ${p.events.length} files, ${duration}ms, ${tracker.steps.length} steps`);
8587
8646
  } catch (err) {
8588
8647
  p.updateIndexState("error", err instanceof Error ? err : new Error(String(err)));
@@ -8598,6 +8657,13 @@ var PipelineRunner = class {
8598
8657
  steps: tracker.steps
8599
8658
  });
8600
8659
  }
8660
+ pipelineActivity.busy = false;
8661
+ pipelineActivity.current_step = null;
8662
+ pipelineActivity.last_completed_at = Date.now();
8663
+ pipelineActivity.last_completed_trigger = "watcher";
8664
+ pipelineActivity.last_completed_duration_ms = duration;
8665
+ pipelineActivity.last_completed_files = p.events.length;
8666
+ pipelineActivity.last_completed_steps = tracker.steps.map((s) => s.name);
8601
8667
  serverLog("watcher", `Failed to rebuild index: ${err instanceof Error ? err.message : err}`, "error");
8602
8668
  }
8603
8669
  }
@@ -8618,6 +8684,7 @@ var PipelineRunner = class {
8618
8684
  };
8619
8685
  const batchResult = await processBatch(vaultIndex2, p.vp, absoluteBatch);
8620
8686
  this.hasEntityRelevantChanges = batchResult.hasEntityRelevantChanges;
8687
+ vaultIndex2.builtAt = /* @__PURE__ */ new Date();
8621
8688
  serverLog("watcher", `Incremental: ${batchResult.successful}/${batchResult.total} files in ${batchResult.durationMs}ms`);
8622
8689
  }
8623
8690
  p.updateIndexState("ready");
@@ -10681,8 +10748,9 @@ var TOOL_CATEGORY = {
10681
10748
  predict_stale_notes: "temporal",
10682
10749
  track_concept_evolution: "temporal",
10683
10750
  temporal_summary: "temporal",
10684
- // diagnostics (20 tools) -- vault health, stats, config, activity, merges, doctor, trust, benchmark, history, learning report, calibration export
10751
+ // diagnostics (21 tools) -- vault health, stats, config, activity, merges, doctor, trust, benchmark, history, learning report, calibration export, pipeline status
10685
10752
  health_check: "diagnostics",
10753
+ pipeline_status: "diagnostics",
10686
10754
  get_vault_stats: "diagnostics",
10687
10755
  get_folder_structure: "diagnostics",
10688
10756
  refresh_index: "diagnostics",
@@ -13982,6 +14050,17 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
13982
14050
  tasks_building: z5.boolean().describe("Whether the task cache is currently rebuilding"),
13983
14051
  watcher_state: z5.enum(["starting", "ready", "rebuilding", "dirty", "error"]).optional().describe("Current file watcher state"),
13984
14052
  watcher_pending: z5.coerce.number().optional().describe("Number of pending file events in the watcher queue"),
14053
+ last_index_activity_at: z5.number().optional().describe("Epoch ms of latest successful index event (any trigger)"),
14054
+ last_index_activity_ago_seconds: z5.coerce.number().optional().describe("Seconds since last successful index event"),
14055
+ last_full_rebuild_at: z5.number().optional().describe("Epoch ms of latest startup_build or manual_refresh event"),
14056
+ last_watcher_batch_at: z5.number().optional().describe("Epoch ms of latest watcher batch event"),
14057
+ pipeline_activity: z5.object({
14058
+ busy: z5.boolean(),
14059
+ current_step: z5.string().nullable(),
14060
+ started_at: z5.number().nullable(),
14061
+ progress: z5.string().nullable(),
14062
+ last_completed_ago_seconds: z5.number().nullable()
14063
+ }).optional().describe("Live pipeline activity state"),
13985
14064
  dead_link_count: z5.coerce.number().describe("Total number of broken/dead wikilinks across the vault"),
13986
14065
  top_dead_link_targets: z5.array(z5.object({
13987
14066
  target: z5.string().describe("The dead link target"),
@@ -14008,11 +14087,14 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
14008
14087
  "health_check",
14009
14088
  {
14010
14089
  title: "Health Check",
14011
- description: "Check MCP server health status. Returns vault accessibility, index freshness, and recommendations. Use at session start to verify MCP is working correctly.",
14012
- inputSchema: {},
14090
+ description: 'Check MCP server health status. Returns vault accessibility, index freshness, and recommendations. Use at session start to verify MCP is working correctly. Pass mode="summary" (default) for lightweight polling or mode="full" for complete diagnostics.',
14091
+ inputSchema: {
14092
+ mode: z5.enum(["summary", "full"]).optional().default("summary").describe('Output mode: "summary" omits config, periodic notes, dead links, sweep, and recent pipelines; "full" returns everything')
14093
+ },
14013
14094
  outputSchema: HealthCheckOutputSchema
14014
14095
  },
14015
- async () => {
14096
+ async ({ mode = "summary" }) => {
14097
+ const isFull = mode === "full";
14016
14098
  const index = getIndex();
14017
14099
  const vaultPath2 = getVaultPath();
14018
14100
  const recommendations = [];
@@ -14043,7 +14125,23 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
14043
14125
  }
14044
14126
  }
14045
14127
  const indexBuilt = indexState2 === "ready" && index !== void 0 && index.notes !== void 0;
14046
- const indexAge = indexBuilt && index.builtAt ? Math.floor((Date.now() - index.builtAt.getTime()) / 1e3) : -1;
14128
+ let lastIndexActivityAt;
14129
+ let lastFullRebuildAt;
14130
+ let lastWatcherBatchAt;
14131
+ if (stateDb2) {
14132
+ try {
14133
+ const lastAny = getLastSuccessfulEvent(stateDb2);
14134
+ if (lastAny) lastIndexActivityAt = lastAny.timestamp;
14135
+ const lastBuild = getLastEventByTrigger(stateDb2, "startup_build");
14136
+ const lastManual = getLastEventByTrigger(stateDb2, "manual_refresh");
14137
+ lastFullRebuildAt = Math.max(lastBuild?.timestamp ?? 0, lastManual?.timestamp ?? 0) || void 0;
14138
+ const lastWatcher = getLastEventByTrigger(stateDb2, "watcher");
14139
+ if (lastWatcher) lastWatcherBatchAt = lastWatcher.timestamp;
14140
+ } catch {
14141
+ }
14142
+ }
14143
+ const freshnessTimestamp = lastIndexActivityAt ?? (indexBuilt && index.builtAt ? index.builtAt.getTime() : void 0);
14144
+ const indexAge = freshnessTimestamp ? Math.floor((Date.now() - freshnessTimestamp) / 1e3) : -1;
14047
14145
  const indexStale = indexBuilt && indexAge > STALE_THRESHOLD_SECONDS;
14048
14146
  if (indexState2 === "building") {
14049
14147
  const { parsed, total } = indexProgress2;
@@ -14073,7 +14171,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
14073
14171
  status = "healthy";
14074
14172
  }
14075
14173
  let periodicNotes;
14076
- if (indexBuilt) {
14174
+ if (isFull && indexBuilt) {
14077
14175
  const types = ["daily", "weekly", "monthly", "quarterly", "yearly"];
14078
14176
  periodicNotes = types.map((type) => {
14079
14177
  const result = detectPeriodicNotes(index, type);
@@ -14087,8 +14185,11 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
14087
14185
  };
14088
14186
  }).filter((p) => p.detected);
14089
14187
  }
14090
- const config = getConfig2();
14091
- const configInfo = Object.keys(config).length > 0 ? config : void 0;
14188
+ let configInfo;
14189
+ if (isFull) {
14190
+ const config = getConfig2();
14191
+ configInfo = Object.keys(config).length > 0 ? config : void 0;
14192
+ }
14092
14193
  let lastRebuild;
14093
14194
  if (stateDb2) {
14094
14195
  try {
@@ -14122,25 +14223,28 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
14122
14223
  }
14123
14224
  } catch {
14124
14225
  }
14125
- try {
14126
- const events = getRecentIndexEvents(stateDb2, 10).filter((e) => e.steps && e.steps.length > 0).slice(0, 5);
14127
- if (events.length > 0) {
14128
- recentPipelines = events.map((e) => ({
14129
- timestamp: e.timestamp,
14130
- trigger: e.trigger,
14131
- duration_ms: e.duration_ms,
14132
- files_changed: e.files_changed,
14133
- changed_paths: e.changed_paths,
14134
- steps: e.steps
14135
- }));
14226
+ if (isFull) {
14227
+ try {
14228
+ const events = getRecentIndexEvents(stateDb2, 10).filter((e) => e.steps && e.steps.length > 0).slice(0, 5);
14229
+ if (events.length > 0) {
14230
+ recentPipelines = events.map((e) => ({
14231
+ timestamp: e.timestamp,
14232
+ trigger: e.trigger,
14233
+ duration_ms: e.duration_ms,
14234
+ files_changed: e.files_changed,
14235
+ changed_paths: e.changed_paths,
14236
+ steps: e.steps
14237
+ }));
14238
+ }
14239
+ } catch {
14136
14240
  }
14137
- } catch {
14138
14241
  }
14139
14242
  }
14140
14243
  const ftsState = getFTS5State();
14141
14244
  let deadLinkCount = 0;
14142
- const deadTargetCounts = /* @__PURE__ */ new Map();
14143
- if (indexBuilt) {
14245
+ let topDeadLinkTargets = [];
14246
+ if (isFull && indexBuilt) {
14247
+ const deadTargetCounts = /* @__PURE__ */ new Map();
14144
14248
  for (const note of index.notes.values()) {
14145
14249
  for (const link of note.outlinks) {
14146
14250
  if (!resolveTarget(index, link.target)) {
@@ -14150,8 +14254,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
14150
14254
  }
14151
14255
  }
14152
14256
  }
14257
+ topDeadLinkTargets = Array.from(deadTargetCounts.entries()).map(([target, mention_count]) => ({ target, mention_count })).sort((a, b) => b.mention_count - a.mention_count).slice(0, 5);
14153
14258
  }
14154
- const topDeadLinkTargets = Array.from(deadTargetCounts.entries()).map(([target, mention_count]) => ({ target, mention_count })).sort((a, b) => b.mention_count - a.mention_count).slice(0, 5);
14155
14259
  let vault_health_score = 0;
14156
14260
  if (indexBuilt && noteCount > 0) {
14157
14261
  const avgOutlinks = linkCount / noteCount;
@@ -14180,6 +14284,14 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
14180
14284
  linkDensity * 25 + orphanRatio * 20 + deadLinkRatio * 15 + fmCoverage * 15 + freshness * 15 + entityCoverage * 10
14181
14285
  );
14182
14286
  }
14287
+ const activity = getPipelineActivity();
14288
+ const pipelineActivity2 = {
14289
+ busy: activity.busy,
14290
+ current_step: activity.current_step,
14291
+ started_at: activity.started_at,
14292
+ progress: activity.busy && activity.total_steps > 0 ? `${activity.completed_steps}/${activity.total_steps} steps` : null,
14293
+ last_completed_ago_seconds: activity.last_completed_at ? Math.floor((Date.now() - activity.last_completed_at) / 1e3) : null
14294
+ };
14183
14295
  const output = {
14184
14296
  status,
14185
14297
  vault_health_score,
@@ -14207,14 +14319,19 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
14207
14319
  embeddings_ready: hasEmbeddingsIndex(),
14208
14320
  embeddings_count: getEmbeddingsCount(),
14209
14321
  embedding_model: hasEmbeddingsIndex() ? getActiveModelId() : void 0,
14210
- embedding_diagnosis: hasEmbeddingsIndex() ? diagnoseEmbeddings(vaultPath2) : void 0,
14322
+ embedding_diagnosis: isFull && hasEmbeddingsIndex() ? diagnoseEmbeddings(vaultPath2) : void 0,
14211
14323
  tasks_ready: isTaskCacheReady(),
14212
14324
  tasks_building: isTaskCacheBuilding(),
14213
14325
  watcher_state: getWatcherStatus2()?.state,
14214
14326
  watcher_pending: getWatcherStatus2()?.pendingEvents,
14327
+ last_index_activity_at: lastIndexActivityAt,
14328
+ last_index_activity_ago_seconds: lastIndexActivityAt ? Math.floor((Date.now() - lastIndexActivityAt) / 1e3) : void 0,
14329
+ last_full_rebuild_at: lastFullRebuildAt,
14330
+ last_watcher_batch_at: lastWatcherBatchAt,
14331
+ pipeline_activity: pipelineActivity2,
14215
14332
  dead_link_count: deadLinkCount,
14216
14333
  top_dead_link_targets: topDeadLinkTargets,
14217
- sweep: getSweepResults() ?? void 0,
14334
+ sweep: isFull ? getSweepResults() ?? void 0 : void 0,
14218
14335
  recommendations
14219
14336
  };
14220
14337
  return {
@@ -14228,6 +14345,56 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
14228
14345
  };
14229
14346
  }
14230
14347
  );
14348
+ server2.registerTool(
14349
+ "pipeline_status",
14350
+ {
14351
+ title: "Pipeline Status",
14352
+ description: "Live pipeline activity: whether a batch is running, current step, and recent completions. Lightweight process-local read \u2014 no DB queries unless detail=true.",
14353
+ inputSchema: {
14354
+ detail: z5.boolean().optional().default(false).describe("Include per-step timings for recent runs")
14355
+ }
14356
+ },
14357
+ async ({ detail = false }) => {
14358
+ const activity = getPipelineActivity();
14359
+ const now = Date.now();
14360
+ const output = {
14361
+ busy: activity.busy,
14362
+ trigger: activity.trigger,
14363
+ started_at: activity.started_at,
14364
+ age_ms: activity.busy && activity.started_at ? now - activity.started_at : null,
14365
+ current_step: activity.current_step,
14366
+ progress: activity.busy && activity.total_steps > 0 ? `${activity.completed_steps}/${activity.total_steps} steps` : null,
14367
+ pending_events: activity.pending_events,
14368
+ last_completed: activity.last_completed_at ? {
14369
+ at: activity.last_completed_at,
14370
+ ago_seconds: Math.floor((now - activity.last_completed_at) / 1e3),
14371
+ trigger: activity.last_completed_trigger,
14372
+ duration_ms: activity.last_completed_duration_ms,
14373
+ files: activity.last_completed_files,
14374
+ steps: activity.last_completed_steps
14375
+ } : null
14376
+ };
14377
+ if (detail) {
14378
+ const stateDb2 = getStateDb3();
14379
+ if (stateDb2) {
14380
+ try {
14381
+ const events = getRecentIndexEvents(stateDb2, 10).filter((e) => e.steps && e.steps.length > 0).slice(0, 5);
14382
+ output.recent_runs = events.map((e) => ({
14383
+ timestamp: e.timestamp,
14384
+ trigger: e.trigger,
14385
+ duration_ms: e.duration_ms,
14386
+ files_changed: e.files_changed,
14387
+ steps: e.steps
14388
+ }));
14389
+ } catch {
14390
+ }
14391
+ }
14392
+ }
14393
+ return {
14394
+ content: [{ type: "text", text: JSON.stringify(output, null, 2) }]
14395
+ };
14396
+ }
14397
+ );
14231
14398
  const TagStatSchema = z5.object({
14232
14399
  tag: z5.string().describe("The tag name"),
14233
14400
  count: z5.coerce.number().describe("Number of notes with this tag")
@@ -14437,18 +14604,28 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
14437
14604
  const indexState2 = getIndexState();
14438
14605
  const indexBuilt = indexState2 === "ready" && index?.notes !== void 0;
14439
14606
  if (indexState2 === "ready" && indexBuilt) {
14440
- const age = Math.floor((Date.now() - index.builtAt.getTime()) / 1e3);
14607
+ let activityAge = null;
14608
+ if (stateDb2) {
14609
+ try {
14610
+ const lastEvt = getLastSuccessfulEvent(stateDb2);
14611
+ if (lastEvt) activityAge = Math.floor((Date.now() - lastEvt.timestamp) / 1e3);
14612
+ } catch {
14613
+ }
14614
+ }
14615
+ const age = activityAge ?? Math.floor((Date.now() - index.builtAt.getTime()) / 1e3);
14441
14616
  if (age > STALE_THRESHOLD_SECONDS) {
14442
- checks.push({ name: "index_freshness", status: "warning", detail: `Index is ${Math.floor(age / 60)} minutes old`, fix: "Run refresh_index to rebuild" });
14617
+ checks.push({ name: "index_activity", status: "warning", detail: `Last index activity ${Math.floor(age / 60)} minutes ago`, fix: "Run refresh_index to rebuild" });
14443
14618
  } else {
14444
- checks.push({ name: "index_freshness", status: "ok", detail: `Index built ${age}s ago, ${index.notes.size} notes, ${index.entities.size} entities` });
14619
+ checks.push({ name: "index_activity", status: "ok", detail: `Last activity ${age}s ago, ${index.notes.size} notes, ${index.entities.size} entities` });
14445
14620
  }
14621
+ const snapshotAge = Math.floor((Date.now() - index.builtAt.getTime()) / 1e3);
14622
+ checks.push({ name: "index_snapshot_age", status: "ok", detail: `In-memory snapshot built ${snapshotAge}s ago` });
14446
14623
  } else if (indexState2 === "building") {
14447
14624
  const progress = getIndexProgress();
14448
- checks.push({ name: "index_freshness", status: "warning", detail: `Index building (${progress.parsed}/${progress.total} files)` });
14625
+ checks.push({ name: "index_activity", status: "warning", detail: `Index building (${progress.parsed}/${progress.total} files)` });
14449
14626
  } else {
14450
14627
  const err = getIndexError();
14451
- checks.push({ name: "index_freshness", status: "error", detail: `Index in ${indexState2} state${err ? ": " + err.message : ""}`, fix: "Run refresh_index" });
14628
+ checks.push({ name: "index_activity", status: "error", detail: `Index in ${indexState2} state${err ? ": " + err.message : ""}`, fix: "Run refresh_index" });
14452
14629
  }
14453
14630
  const embReady = hasEmbeddingsIndex();
14454
14631
  const embCount = getEmbeddingsCount();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@velvetmonkey/flywheel-memory",
3
- "version": "2.1.3",
4
- "description": "MCP tools that search, write, and auto-link your Obsidian vault \u2014 and learn from your edits.",
3
+ "version": "2.1.4",
4
+ "description": "MCP tools that search, write, and auto-link your Obsidian vault and learn from your edits.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
@@ -54,7 +54,7 @@
54
54
  "dependencies": {
55
55
  "@huggingface/transformers": "^3.8.1",
56
56
  "@modelcontextprotocol/sdk": "^1.25.1",
57
- "@velvetmonkey/vault-core": "^2.1.3",
57
+ "@velvetmonkey/vault-core": "^2.1.4",
58
58
  "better-sqlite3": "^12.0.0",
59
59
  "chokidar": "^4.0.0",
60
60
  "gray-matter": "^4.0.3",