@velvetmonkey/flywheel-memory 2.5.9 → 2.5.10

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 +642 -312
  2. package/package.json +3 -3
package/dist/index.js CHANGED
@@ -2672,10 +2672,10 @@ function isLockContentionError(error) {
2672
2672
  function sleep(ms) {
2673
2673
  return new Promise((resolve2) => setTimeout(resolve2, ms));
2674
2674
  }
2675
- function calculateDelay(attempt, config) {
2676
- let delay = config.baseDelayMs * Math.pow(2, attempt);
2677
- delay = Math.min(delay, config.maxDelayMs);
2678
- if (config.jitter) {
2675
+ function calculateDelay(attempt, config2) {
2676
+ let delay = config2.baseDelayMs * Math.pow(2, attempt);
2677
+ delay = Math.min(delay, config2.maxDelayMs);
2678
+ if (config2.jitter) {
2679
2679
  delay = delay + Math.random() * delay * 0.5;
2680
2680
  }
2681
2681
  return Math.round(delay);
@@ -3569,8 +3569,8 @@ function setWriteStateDb(stateDb2) {
3569
3569
  function getWriteStateDb() {
3570
3570
  return getActiveScopeOrNull()?.stateDb ?? moduleStateDb5;
3571
3571
  }
3572
- function setWikilinkConfig(config) {
3573
- moduleConfig = config;
3572
+ function setWikilinkConfig(config2) {
3573
+ moduleConfig = config2;
3574
3574
  }
3575
3575
  function getConfig() {
3576
3576
  const scope = getActiveScopeOrNull();
@@ -3938,9 +3938,9 @@ function isCommonWordFalsePositive(entityName, rawContent, category) {
3938
3938
  if (!IMPLICIT_EXCLUDE_WORDS.has(lowerName) && !COMMON_ENGLISH_WORDS.has(lowerName)) return false;
3939
3939
  return !rawContent.includes(entityName);
3940
3940
  }
3941
- function capScoreWithoutContentRelevance(score, contentRelevance, config) {
3942
- if (contentRelevance < config.contentRelevanceFloor) {
3943
- return Math.min(score, config.noRelevanceCap);
3941
+ function capScoreWithoutContentRelevance(score, contentRelevance, config2) {
3942
+ if (contentRelevance < config2.contentRelevanceFloor) {
3943
+ return Math.min(score, config2.noRelevanceCap);
3944
3944
  }
3945
3945
  return score;
3946
3946
  }
@@ -3988,7 +3988,7 @@ function getAdaptiveMinScore(contentLength, baseScore) {
3988
3988
  }
3989
3989
  return baseScore;
3990
3990
  }
3991
- function scoreNameAgainstContent(name, contentTokens, contentStems, config, coocIndex, disableExact, disableStem) {
3991
+ function scoreNameAgainstContent(name, contentTokens, contentStems, config2, coocIndex, disableExact, disableStem) {
3992
3992
  const nameTokens = tokenize(name);
3993
3993
  if (nameTokens.length === 0) {
3994
3994
  return { exactScore: 0, stemScore: 0, lexicalScore: 0, matchedWords: 0, exactMatches: 0, totalTokens: 0, nameTokens: [], unmatchedTokenIndices: [] };
@@ -4004,11 +4004,11 @@ function scoreNameAgainstContent(name, contentTokens, contentStems, config, cooc
4004
4004
  const nameStem = nameStems[i];
4005
4005
  const idfWeight = coocIndex ? tokenIdf(token, coocIndex) : 1;
4006
4006
  if (!disableExact && contentTokens.has(token)) {
4007
- exactScore += config.exactMatchBonus * idfWeight;
4007
+ exactScore += config2.exactMatchBonus * idfWeight;
4008
4008
  matchedWords++;
4009
4009
  exactMatches++;
4010
4010
  } else if (!disableStem && contentStems.has(nameStem)) {
4011
- stemScore += config.stemMatchBonus * idfWeight;
4011
+ stemScore += config2.stemMatchBonus * idfWeight;
4012
4012
  matchedWords++;
4013
4013
  } else {
4014
4014
  unmatchedTokenIndices.push(i);
@@ -4017,7 +4017,7 @@ function scoreNameAgainstContent(name, contentTokens, contentStems, config, cooc
4017
4017
  const lexicalScore = Math.round((exactScore + stemScore) * 10) / 10;
4018
4018
  return { exactScore, stemScore, lexicalScore, matchedWords, exactMatches, totalTokens: nameTokens.length, nameTokens, unmatchedTokenIndices };
4019
4019
  }
4020
- function scoreEntity(entity, contentTokens, contentStems, collapsedContentTerms, config, disabled, coocIndex, tokenFuzzyCache) {
4020
+ function scoreEntity(entity, contentTokens, contentStems, collapsedContentTerms, config2, disabled, coocIndex, tokenFuzzyCache) {
4021
4021
  const zero = { contentMatch: 0, fuzzyMatch: 0, totalLexical: 0, matchedWords: 0, exactMatches: 0, totalTokens: 0 };
4022
4022
  const entityName = getEntityName2(entity);
4023
4023
  const aliases = getEntityAliases(entity);
@@ -4026,10 +4026,10 @@ function scoreEntity(entity, contentTokens, contentStems, collapsedContentTerms,
4026
4026
  const disableFuzzy = disabled.has("fuzzy_match");
4027
4027
  const cache = tokenFuzzyCache ?? /* @__PURE__ */ new Map();
4028
4028
  const idfFn = (token) => coocIndex ? tokenIdf(token, coocIndex) : 1;
4029
- const nameResult = scoreNameAgainstContent(entityName, contentTokens, contentStems, config, coocIndex, disableExact, disableStem);
4029
+ const nameResult = scoreNameAgainstContent(entityName, contentTokens, contentStems, config2, coocIndex, disableExact, disableStem);
4030
4030
  let bestAliasResult = { exactScore: 0, stemScore: 0, lexicalScore: 0, matchedWords: 0, exactMatches: 0, totalTokens: 0, nameTokens: [], unmatchedTokenIndices: [] };
4031
4031
  for (const alias of aliases) {
4032
- const aliasResult = scoreNameAgainstContent(alias, contentTokens, contentStems, config, coocIndex, disableExact, disableStem);
4032
+ const aliasResult = scoreNameAgainstContent(alias, contentTokens, contentStems, config2, coocIndex, disableExact, disableStem);
4033
4033
  if (aliasResult.lexicalScore > bestAliasResult.lexicalScore) {
4034
4034
  bestAliasResult = aliasResult;
4035
4035
  }
@@ -4056,7 +4056,7 @@ function scoreEntity(entity, contentTokens, contentStems, collapsedContentTerms,
4056
4056
  contentTokens,
4057
4057
  collapsedContentTerms,
4058
4058
  fuzzyTargetName,
4059
- config.fuzzyMatchBonus,
4059
+ config2.fuzzyMatchBonus,
4060
4060
  idfFn,
4061
4061
  cache
4062
4062
  );
@@ -4070,11 +4070,11 @@ function scoreEntity(entity, contentTokens, contentStems, collapsedContentTerms,
4070
4070
  }
4071
4071
  if (totalTokens > 1) {
4072
4072
  const matchRatio = matchedWords / totalTokens;
4073
- if (matchRatio < config.minMatchRatio) {
4073
+ if (matchRatio < config2.minMatchRatio) {
4074
4074
  return zero;
4075
4075
  }
4076
4076
  }
4077
- if (config.requireMultipleMatches && totalTokens === 1) {
4077
+ if (config2.requireMultipleMatches && totalTokens === 1) {
4078
4078
  if (exactMatches === 0 && fuzzyMatchedWords === 0) {
4079
4079
  return zero;
4080
4080
  }
@@ -4105,8 +4105,8 @@ async function suggestRelatedLinks(content, options = {}) {
4105
4105
  disabledLayers = []
4106
4106
  } = options;
4107
4107
  const disabled = new Set(disabledLayers);
4108
- const config = STRICTNESS_CONFIGS[strictness];
4109
- const adaptiveMinScore = getAdaptiveMinScore(content.length, config.minSuggestionScore);
4108
+ const config2 = STRICTNESS_CONFIGS[strictness];
4109
+ const adaptiveMinScore = getAdaptiveMinScore(content.length, config2.minSuggestionScore);
4110
4110
  const noteContext = notePath ? getNoteContext(notePath) : "general";
4111
4111
  const contextBoosts = CONTEXT_BOOST[noteContext];
4112
4112
  const emptyResult = { suggestions: [], suffix: "" };
@@ -4128,7 +4128,7 @@ async function suggestRelatedLinks(content, options = {}) {
4128
4128
  const contentTokens = /* @__PURE__ */ new Set();
4129
4129
  const contentStems = /* @__PURE__ */ new Set();
4130
4130
  for (const token of rawTokens) {
4131
- if (token.length >= config.minWordLength && !STOPWORDS_EN2.has(token)) {
4131
+ if (token.length >= config2.minWordLength && !STOPWORDS_EN2.has(token)) {
4132
4132
  contentTokens.add(token);
4133
4133
  contentStems.add(stem(token));
4134
4134
  }
@@ -4136,7 +4136,7 @@ async function suggestRelatedLinks(content, options = {}) {
4136
4136
  if (contentTokens.size === 0) {
4137
4137
  return emptyResult;
4138
4138
  }
4139
- const orderedContentTokens = [...rawTokens].filter((token) => token.length >= config.minWordLength && !STOPWORDS_EN2.has(token)).map(normalizeFuzzyTerm).filter((token) => token.length > 0);
4139
+ const orderedContentTokens = [...rawTokens].filter((token) => token.length >= config2.minWordLength && !STOPWORDS_EN2.has(token)).map(normalizeFuzzyTerm).filter((token) => token.length > 0);
4140
4140
  const collapsedContentTerms = disabled.has("fuzzy_match") ? /* @__PURE__ */ new Set() : buildCollapsedContentTerms(orderedContentTokens);
4141
4141
  const tokenFuzzyCache = /* @__PURE__ */ new Map();
4142
4142
  const linkedEntities = excludeLinked ? extractLinkedEntities(content) : /* @__PURE__ */ new Set();
@@ -4167,7 +4167,7 @@ async function suggestRelatedLinks(content, options = {}) {
4167
4167
  const paths = correctedPairs.get(entityName.toLowerCase());
4168
4168
  if (paths.has(notePath)) continue;
4169
4169
  }
4170
- const entityScore = disabled.has("exact_match") && disabled.has("stem_match") && disabled.has("fuzzy_match") ? { contentMatch: 0, fuzzyMatch: 0, totalLexical: 0, matchedWords: 0, exactMatches: 0, totalTokens: 0 } : scoreEntity(entity, contentTokens, contentStems, collapsedContentTerms, config, disabled, cooccurrenceIndex, tokenFuzzyCache);
4170
+ const entityScore = disabled.has("exact_match") && disabled.has("stem_match") && disabled.has("fuzzy_match") ? { contentMatch: 0, fuzzyMatch: 0, totalLexical: 0, matchedWords: 0, exactMatches: 0, totalTokens: 0 } : scoreEntity(entity, contentTokens, contentStems, collapsedContentTerms, config2, disabled, cooccurrenceIndex, tokenFuzzyCache);
4171
4171
  const contentScore = entityScore.contentMatch;
4172
4172
  const fuzzyMatchScore = entityScore.fuzzyMatch;
4173
4173
  const hasLexicalEvidence = entityScore.totalLexical > 0;
@@ -4204,7 +4204,7 @@ async function suggestRelatedLinks(content, options = {}) {
4204
4204
  }
4205
4205
  const layerSuppressionPenalty = disabled.has("feedback") ? 0 : suppressionPenalties.get(entityName) ?? 0;
4206
4206
  score += layerSuppressionPenalty;
4207
- score = capScoreWithoutContentRelevance(score, contentScore + fuzzyMatchScore, config);
4207
+ score = capScoreWithoutContentRelevance(score, contentScore + fuzzyMatchScore, config2);
4208
4208
  if (hasLexicalEvidence && score >= adaptiveMinScore) {
4209
4209
  scoredEntities.push({
4210
4210
  name: entityName,
@@ -4271,13 +4271,13 @@ async function suggestRelatedLinks(content, options = {}) {
4271
4271
  existing.score += boost;
4272
4272
  existing.breakdown.cooccurrenceBoost += boost;
4273
4273
  const existingContentRelevance = existing.breakdown.contentMatch + existing.breakdown.fuzzyMatch + (existing.breakdown.semanticBoost ?? 0);
4274
- existing.score = capScoreWithoutContentRelevance(existing.score, existingContentRelevance, config);
4274
+ existing.score = capScoreWithoutContentRelevance(existing.score, existingContentRelevance, config2);
4275
4275
  } else {
4276
4276
  const entityTokens = tokenize(entityName);
4277
4277
  const hasContentOverlap = entityTokens.some(
4278
4278
  (token) => contentTokens.has(token) || contentStems.has(stem(token))
4279
4279
  );
4280
- const strongCooccurrence = boost >= config.minCooccurrenceGate;
4280
+ const strongCooccurrence = boost >= config2.minCooccurrenceGate;
4281
4281
  if (!hasContentOverlap && !strongCooccurrence) {
4282
4282
  continue;
4283
4283
  }
@@ -4297,7 +4297,7 @@ async function suggestRelatedLinks(content, options = {}) {
4297
4297
  const suppPenalty = disabled.has("feedback") ? 0 : suppressionPenalties.get(entityName) ?? 0;
4298
4298
  let totalBoost = boost + typeBoost + contextBoost + recencyBoostVal + crossFolderBoost + hubBoost + feedbackAdj + edgeWeightBoost + prospectBoost + suppPenalty;
4299
4299
  const coocContentRelevance = hasContentOverlap ? 5 : 0;
4300
- totalBoost = capScoreWithoutContentRelevance(totalBoost, coocContentRelevance, config);
4300
+ totalBoost = capScoreWithoutContentRelevance(totalBoost, coocContentRelevance, config2);
4301
4301
  const effectiveMinScore = !hasContentOverlap ? Math.max(adaptiveMinScore, 7) : adaptiveMinScore;
4302
4302
  if (totalBoost >= effectiveMinScore) {
4303
4303
  scoredEntities.push({
@@ -4393,11 +4393,11 @@ async function suggestRelatedLinks(content, options = {}) {
4393
4393
  }
4394
4394
  for (const entry of scoredEntities) {
4395
4395
  const contentRelevance = entry.breakdown.contentMatch + entry.breakdown.fuzzyMatch + (entry.breakdown.semanticBoost ?? 0);
4396
- entry.score = capScoreWithoutContentRelevance(entry.score, contentRelevance, config);
4396
+ entry.score = capScoreWithoutContentRelevance(entry.score, contentRelevance, config2);
4397
4397
  }
4398
4398
  const relevantEntities = scoredEntities.filter((e) => {
4399
4399
  if (!entitiesWithAnyScoringPath.has(e.name)) return false;
4400
- if (config.minContentMatch > 0 && e.breakdown.contentMatch < config.minContentMatch) return false;
4400
+ if (config2.minContentMatch > 0 && e.breakdown.contentMatch < config2.minContentMatch) return false;
4401
4401
  return true;
4402
4402
  });
4403
4403
  if (relevantEntities.length === 0) {
@@ -4664,9 +4664,9 @@ async function checkPreflightSimilarity(noteName) {
4664
4664
  }
4665
4665
  return result;
4666
4666
  }
4667
- async function applyProactiveSuggestions(filePath, vaultPath2, suggestions, config) {
4667
+ async function applyProactiveSuggestions(filePath, vaultPath2, suggestions, config2) {
4668
4668
  const stateDb2 = getWriteStateDb();
4669
- const candidates = suggestions.filter((s) => s.score >= config.minScore && s.confidence === "high").slice(0, config.maxPerFile);
4669
+ const candidates = suggestions.filter((s) => s.score >= config2.minScore && s.confidence === "high").slice(0, config2.maxPerFile);
4670
4670
  if (candidates.length === 0) {
4671
4671
  return { applied: [], skipped: [] };
4672
4672
  }
@@ -4973,7 +4973,7 @@ function enqueueProactiveSuggestions(stateDb2, entries) {
4973
4973
  }
4974
4974
  return enqueued;
4975
4975
  }
4976
- async function drainProactiveQueue(stateDb2, vaultPath2, config, applyFn) {
4976
+ async function drainProactiveQueue(stateDb2, vaultPath2, config2, applyFn) {
4977
4977
  const result = {
4978
4978
  applied: [],
4979
4979
  expired: 0,
@@ -5017,7 +5017,7 @@ async function drainProactiveQueue(stateDb2, vaultPath2, config, applyFn) {
5017
5017
  continue;
5018
5018
  }
5019
5019
  const todayCount = countTodayApplied.get(filePath, todayStr).cnt;
5020
- if (todayCount >= config.maxPerDay) {
5020
+ if (todayCount >= config2.maxPerDay) {
5021
5021
  result.skippedDailyCap += suggestions.length;
5022
5022
  for (const s of suggestions) {
5023
5023
  try {
@@ -5029,10 +5029,10 @@ async function drainProactiveQueue(stateDb2, vaultPath2, config, applyFn) {
5029
5029
  }
5030
5030
  continue;
5031
5031
  }
5032
- const remaining = config.maxPerDay - todayCount;
5032
+ const remaining = config2.maxPerDay - todayCount;
5033
5033
  const capped = suggestions.slice(0, remaining);
5034
5034
  try {
5035
- const applyResult = await applyFn(filePath, vaultPath2, capped, config);
5035
+ const applyResult = await applyFn(filePath, vaultPath2, capped, config2);
5036
5036
  if (applyResult.applied.length > 0) {
5037
5037
  result.applied.push({ file: filePath, entities: applyResult.applied });
5038
5038
  const appliedAt = Date.now();
@@ -7375,21 +7375,21 @@ var DEFAULT_CONFIG = {
7375
7375
  implicit_detection: true,
7376
7376
  adaptive_strictness: true
7377
7377
  };
7378
- function migrateExcludeConfig(config) {
7378
+ function migrateExcludeConfig(config2) {
7379
7379
  const oldTags = [
7380
- ...config.exclude_task_tags ?? [],
7381
- ...config.exclude_analysis_tags ?? []
7380
+ ...config2.exclude_task_tags ?? [],
7381
+ ...config2.exclude_analysis_tags ?? []
7382
7382
  ];
7383
- const oldEntities = config.exclude_entities ?? [];
7384
- if (oldTags.length === 0 && oldEntities.length === 0) return config;
7383
+ const oldEntities = config2.exclude_entities ?? [];
7384
+ if (oldTags.length === 0 && oldEntities.length === 0) return config2;
7385
7385
  const normalizedTags = oldTags.map((t) => t.startsWith("#") ? t : `#${t}`);
7386
7386
  const merged = /* @__PURE__ */ new Set([
7387
- ...config.exclude ?? [],
7387
+ ...config2.exclude ?? [],
7388
7388
  ...normalizedTags,
7389
7389
  ...oldEntities
7390
7390
  ]);
7391
7391
  return {
7392
- ...config,
7392
+ ...config2,
7393
7393
  exclude: Array.from(merged),
7394
7394
  // Clear deprecated fields
7395
7395
  exclude_task_tags: void 0,
@@ -7488,11 +7488,11 @@ function inferConfig(index, vaultPath2) {
7488
7488
  }
7489
7489
  return inferred;
7490
7490
  }
7491
- function getExcludeTags(config) {
7492
- return (config.exclude ?? []).filter((e) => e.startsWith("#")).map((e) => e.slice(1));
7491
+ function getExcludeTags(config2) {
7492
+ return (config2.exclude ?? []).filter((e) => e.startsWith("#")).map((e) => e.slice(1));
7493
7493
  }
7494
- function getExcludeEntities(config) {
7495
- return (config.exclude ?? []).filter((e) => !e.startsWith("#"));
7494
+ function getExcludeEntities(config2) {
7495
+ return (config2.exclude ?? []).filter((e) => !e.startsWith("#"));
7496
7496
  }
7497
7497
  var TEMPLATE_PATTERNS = {
7498
7498
  daily: /^daily[\s._-]*(note|template)?\.md$/i,
@@ -7812,8 +7812,8 @@ var EventQueue = class {
7812
7812
  config;
7813
7813
  flushTimer = null;
7814
7814
  onBatch;
7815
- constructor(config, onBatch) {
7816
- this.config = config;
7815
+ constructor(config2, onBatch) {
7816
+ this.config = config2;
7817
7817
  this.onBatch = onBatch;
7818
7818
  }
7819
7819
  /**
@@ -8226,7 +8226,7 @@ init_serverLog();
8226
8226
  // src/core/read/watch/index.ts
8227
8227
  function createVaultWatcher(options) {
8228
8228
  const { vaultPath: vaultPath2, onBatch, onStateChange, onError } = options;
8229
- const config = {
8229
+ const config2 = {
8230
8230
  ...DEFAULT_WATCHER_CONFIG,
8231
8231
  ...parseWatcherConfig(),
8232
8232
  ...options.config
@@ -8271,7 +8271,7 @@ function createVaultWatcher(options) {
8271
8271
  }
8272
8272
  }
8273
8273
  };
8274
- const eventQueue = new EventQueue(config, processBatch2);
8274
+ const eventQueue = new EventQueue(config2, processBatch2);
8275
8275
  const instance = {
8276
8276
  get status() {
8277
8277
  return getStatus();
@@ -8284,8 +8284,8 @@ function createVaultWatcher(options) {
8284
8284
  console.error("[flywheel] Watcher already started");
8285
8285
  return;
8286
8286
  }
8287
- console.error(`[flywheel] Starting file watcher (debounce: ${config.debounceMs}ms, flush: ${config.flushMs}ms)`);
8288
- console.error(`[flywheel] Chokidar options: usePolling=${config.usePolling}, interval=${config.pollInterval}, vaultPath=${vaultPath2}`);
8287
+ console.error(`[flywheel] Starting file watcher (debounce: ${config2.debounceMs}ms, flush: ${config2.flushMs}ms)`);
8288
+ console.error(`[flywheel] Chokidar options: usePolling=${config2.usePolling}, interval=${config2.pollInterval}, vaultPath=${vaultPath2}`);
8289
8289
  watcher = chokidar.watch(vaultPath2, {
8290
8290
  ignored: createIgnoreFunction(vaultPath2),
8291
8291
  persistent: true,
@@ -8294,8 +8294,8 @@ function createVaultWatcher(options) {
8294
8294
  stabilityThreshold: 300,
8295
8295
  pollInterval: 100
8296
8296
  },
8297
- usePolling: config.usePolling,
8298
- interval: config.usePolling ? config.pollInterval : void 0
8297
+ usePolling: config2.usePolling,
8298
+ interval: config2.usePolling ? config2.pollInterval : void 0
8299
8299
  });
8300
8300
  watcher.on("add", (path39) => {
8301
8301
  console.error(`[flywheel] RAW EVENT: add ${path39}`);
@@ -9479,6 +9479,104 @@ function refreshIfStale(vaultPath2, index, excludeTags) {
9479
9479
  init_wikilinkFeedback();
9480
9480
  init_corrections();
9481
9481
  init_edgeWeights();
9482
+ var DeferredStepScheduler = class {
9483
+ timers = /* @__PURE__ */ new Map();
9484
+ executor = null;
9485
+ /** Set the executor context (called once during watcher setup) */
9486
+ setExecutor(exec) {
9487
+ this.executor = exec;
9488
+ }
9489
+ /** Schedule a deferred step to run after delayMs. Cancels any existing timer for this step. */
9490
+ schedule(step, delayMs) {
9491
+ this.cancel(step);
9492
+ const timer2 = setTimeout(() => {
9493
+ this.timers.delete(step);
9494
+ this.executeStep(step);
9495
+ }, delayMs);
9496
+ timer2.unref();
9497
+ this.timers.set(step, timer2);
9498
+ serverLog("deferred", `Scheduled ${step} in ${Math.round(delayMs / 1e3)}s`);
9499
+ }
9500
+ /** Cancel a pending deferred step */
9501
+ cancel(step) {
9502
+ const existing = this.timers.get(step);
9503
+ if (existing) {
9504
+ clearTimeout(existing);
9505
+ this.timers.delete(step);
9506
+ }
9507
+ }
9508
+ /** Cancel all pending deferred steps (called on shutdown) */
9509
+ cancelAll() {
9510
+ for (const timer2 of this.timers.values()) clearTimeout(timer2);
9511
+ this.timers.clear();
9512
+ }
9513
+ /** Check if any steps are pending */
9514
+ get pendingCount() {
9515
+ return this.timers.size;
9516
+ }
9517
+ async executeStep(step) {
9518
+ const exec = this.executor;
9519
+ if (!exec) return;
9520
+ if (exec.ctx.pipelineActivity.busy) {
9521
+ serverLog("deferred", `Skipping ${step}: pipeline busy`);
9522
+ return;
9523
+ }
9524
+ const start = Date.now();
9525
+ try {
9526
+ switch (step) {
9527
+ case "entity_scan": {
9528
+ await exec.updateEntitiesInStateDb(exec.vp, exec.sd);
9529
+ exec.ctx.lastEntityScanAt = Date.now();
9530
+ if (exec.sd) {
9531
+ await exportHubScores(exec.getVaultIndex(), exec.sd);
9532
+ exec.ctx.lastHubScoreRebuildAt = Date.now();
9533
+ }
9534
+ break;
9535
+ }
9536
+ case "hub_scores": {
9537
+ await exportHubScores(exec.getVaultIndex(), exec.sd);
9538
+ exec.ctx.lastHubScoreRebuildAt = Date.now();
9539
+ break;
9540
+ }
9541
+ case "recency": {
9542
+ const entities = exec.sd ? getAllEntitiesFromDb(exec.sd) : [];
9543
+ const entityInput = entities.map((e) => ({ name: e.name, path: e.path, aliases: e.aliases }));
9544
+ const recencyIndex2 = await buildRecencyIndex(exec.vp, entityInput);
9545
+ saveRecencyToStateDb(recencyIndex2, exec.sd ?? void 0);
9546
+ break;
9547
+ }
9548
+ case "cooccurrence": {
9549
+ const entities = exec.sd ? getAllEntitiesFromDb(exec.sd) : [];
9550
+ const entityNames = entities.map((e) => e.name);
9551
+ const cooccurrenceIdx = await mineCooccurrences(exec.vp, entityNames);
9552
+ setCooccurrenceIndex(cooccurrenceIdx);
9553
+ exec.ctx.lastCooccurrenceRebuildAt = Date.now();
9554
+ exec.ctx.cooccurrenceIndex = cooccurrenceIdx;
9555
+ if (exec.sd) saveCooccurrenceToStateDb(exec.sd, cooccurrenceIdx);
9556
+ break;
9557
+ }
9558
+ case "edge_weights": {
9559
+ if (exec.sd) {
9560
+ recomputeEdgeWeights(exec.sd);
9561
+ exec.ctx.lastEdgeWeightRebuildAt = Date.now();
9562
+ }
9563
+ break;
9564
+ }
9565
+ }
9566
+ const duration = Date.now() - start;
9567
+ serverLog("deferred", `Completed ${step} in ${duration}ms`);
9568
+ if (exec.sd) {
9569
+ recordIndexEvent(exec.sd, {
9570
+ trigger: "deferred",
9571
+ duration_ms: duration,
9572
+ note_count: exec.getVaultIndex().notes.size
9573
+ });
9574
+ }
9575
+ } catch (err) {
9576
+ serverLog("deferred", `Failed ${step}: ${err instanceof Error ? err.message : err}`, "error");
9577
+ }
9578
+ }
9579
+ };
9482
9580
  var PIPELINE_TOTAL_STEPS = 22;
9483
9581
  function createEmptyPipelineActivity() {
9484
9582
  return {
@@ -9706,6 +9804,7 @@ var PipelineRunner = class {
9706
9804
  tracker.skip("entity_scan", `cache valid (${Math.round(entityScanAgeMs / 1e3)}s old)`);
9707
9805
  this.entitiesBefore = p.sd ? getAllEntitiesFromDb(p.sd) : [];
9708
9806
  this.entitiesAfter = this.entitiesBefore;
9807
+ p.deferredScheduler?.schedule("entity_scan", 5 * 60 * 1e3 - entityScanAgeMs);
9709
9808
  serverLog("watcher", `Entity scan: throttled (${Math.round(entityScanAgeMs / 1e3)}s old)`);
9710
9809
  return;
9711
9810
  }
@@ -9751,6 +9850,7 @@ var PipelineRunner = class {
9751
9850
  const { p } = this;
9752
9851
  const hubAgeMs = p.ctx.lastHubScoreRebuildAt > 0 ? Date.now() - p.ctx.lastHubScoreRebuildAt : Infinity;
9753
9852
  if (hubAgeMs < 5 * 60 * 1e3) {
9853
+ p.deferredScheduler?.schedule("hub_scores", 5 * 60 * 1e3 - hubAgeMs);
9754
9854
  serverLog("watcher", `Hub scores: throttled (${Math.round(hubAgeMs / 1e3)}s old)`);
9755
9855
  return { skipped: true, age_ms: hubAgeMs };
9756
9856
  }
@@ -9780,6 +9880,7 @@ var PipelineRunner = class {
9780
9880
  serverLog("watcher", `Recency: rebuilt ${recencyIndex2.lastMentioned.size} entities`);
9781
9881
  return { rebuilt: true, entities: recencyIndex2.lastMentioned.size };
9782
9882
  }
9883
+ p.deferredScheduler?.schedule("recency", 60 * 60 * 1e3 - cacheAgeMs);
9783
9884
  serverLog("watcher", `Recency: cache valid (${Math.round(cacheAgeMs / 1e3)}s old)`);
9784
9885
  return { rebuilt: false, cached_age_ms: cacheAgeMs };
9785
9886
  }
@@ -9799,6 +9900,7 @@ var PipelineRunner = class {
9799
9900
  serverLog("watcher", `Co-occurrence: rebuilt ${cooccurrenceIdx._metadata.total_associations} associations`);
9800
9901
  return { rebuilt: true, associations: cooccurrenceIdx._metadata.total_associations };
9801
9902
  }
9903
+ p.deferredScheduler?.schedule("cooccurrence", 60 * 60 * 1e3 - cooccurrenceAgeMs);
9802
9904
  serverLog("watcher", `Co-occurrence: cache valid (${Math.round(cooccurrenceAgeMs / 1e3)}s old)`);
9803
9905
  return { rebuilt: false, age_ms: cooccurrenceAgeMs };
9804
9906
  }
@@ -9821,6 +9923,7 @@ var PipelineRunner = class {
9821
9923
  top_changes: result.top_changes
9822
9924
  };
9823
9925
  }
9926
+ p.deferredScheduler?.schedule("edge_weights", 60 * 60 * 1e3 - edgeWeightAgeMs);
9824
9927
  serverLog("watcher", `Edge weights: cache valid (${Math.round(edgeWeightAgeMs / 1e3)}s old)`);
9825
9928
  return { rebuilt: false, age_ms: edgeWeightAgeMs };
9826
9929
  }
@@ -10810,7 +10913,7 @@ function getToolSelectionReport(stateDb2, daysBack = 7) {
10810
10913
  }
10811
10914
 
10812
10915
  // src/index.ts
10813
- import { openStateDb, scanVaultEntities as scanVaultEntities4, getAllEntitiesFromDb as getAllEntitiesFromDb5, loadContentHashes, saveContentHashBatch, renameContentHash, checkDbIntegrity as checkDbIntegrity2, safeBackupAsync as safeBackupAsync2, preserveCorruptedDb, deleteStateDbFiles, attemptSalvage } from "@velvetmonkey/vault-core";
10916
+ import { openStateDb, scanVaultEntities as scanVaultEntities5, getAllEntitiesFromDb as getAllEntitiesFromDb6, loadContentHashes, saveContentHashBatch, renameContentHash, checkDbIntegrity as checkDbIntegrity2, safeBackupAsync as safeBackupAsync2, preserveCorruptedDb, deleteStateDbFiles, attemptSalvage } from "@velvetmonkey/vault-core";
10814
10917
 
10815
10918
  // src/core/write/memory.ts
10816
10919
  init_wikilinkFeedback();
@@ -11229,6 +11332,382 @@ function getSweepResults() {
11229
11332
  return cachedResults;
11230
11333
  }
11231
11334
 
11335
+ // src/core/read/watch/maintenance.ts
11336
+ init_serverLog();
11337
+ import { getAllEntitiesFromDb as getAllEntitiesFromDb2 } from "@velvetmonkey/vault-core";
11338
+ init_recency();
11339
+ init_cooccurrence();
11340
+ init_wikilinks();
11341
+ init_edgeWeights();
11342
+
11343
+ // src/core/shared/graphSnapshots.ts
11344
+ function computeGraphMetrics(index) {
11345
+ const noteCount = index.notes.size;
11346
+ if (noteCount === 0) {
11347
+ return {
11348
+ avg_degree: 0,
11349
+ max_degree: 0,
11350
+ cluster_count: 0,
11351
+ largest_cluster_size: 0,
11352
+ hub_scores_top10: []
11353
+ };
11354
+ }
11355
+ const degreeMap = /* @__PURE__ */ new Map();
11356
+ const adjacency = /* @__PURE__ */ new Map();
11357
+ for (const [notePath, note] of index.notes) {
11358
+ if (!adjacency.has(notePath)) adjacency.set(notePath, /* @__PURE__ */ new Set());
11359
+ let degree = note.outlinks.length;
11360
+ for (const link of note.outlinks) {
11361
+ const targetLower = link.target.toLowerCase();
11362
+ const resolvedPath = index.entities.get(targetLower);
11363
+ if (resolvedPath && index.notes.has(resolvedPath)) {
11364
+ adjacency.get(notePath).add(resolvedPath);
11365
+ if (!adjacency.has(resolvedPath)) adjacency.set(resolvedPath, /* @__PURE__ */ new Set());
11366
+ adjacency.get(resolvedPath).add(notePath);
11367
+ }
11368
+ }
11369
+ degreeMap.set(notePath, degree);
11370
+ }
11371
+ for (const [target, backlinks] of index.backlinks) {
11372
+ const targetLower = target.toLowerCase();
11373
+ const resolvedPath = index.entities.get(targetLower);
11374
+ if (resolvedPath && degreeMap.has(resolvedPath)) {
11375
+ degreeMap.set(resolvedPath, degreeMap.get(resolvedPath) + backlinks.length);
11376
+ }
11377
+ }
11378
+ let totalDegree = 0;
11379
+ let maxDegree = 0;
11380
+ let maxDegreeNote = "";
11381
+ for (const [notePath, degree] of degreeMap) {
11382
+ totalDegree += degree;
11383
+ if (degree > maxDegree) {
11384
+ maxDegree = degree;
11385
+ maxDegreeNote = notePath;
11386
+ }
11387
+ }
11388
+ const avgDegree = noteCount > 0 ? Math.round(totalDegree / noteCount * 100) / 100 : 0;
11389
+ const visited = /* @__PURE__ */ new Set();
11390
+ const clusters = [];
11391
+ for (const notePath of index.notes.keys()) {
11392
+ if (visited.has(notePath)) continue;
11393
+ const queue = [notePath];
11394
+ visited.add(notePath);
11395
+ let clusterSize = 0;
11396
+ while (queue.length > 0) {
11397
+ const current = queue.shift();
11398
+ clusterSize++;
11399
+ const neighbors = adjacency.get(current);
11400
+ if (neighbors) {
11401
+ for (const neighbor of neighbors) {
11402
+ if (!visited.has(neighbor)) {
11403
+ visited.add(neighbor);
11404
+ queue.push(neighbor);
11405
+ }
11406
+ }
11407
+ }
11408
+ }
11409
+ clusters.push(clusterSize);
11410
+ }
11411
+ const clusterCount = clusters.length;
11412
+ const largestClusterSize = clusters.length > 0 ? Math.max(...clusters) : 0;
11413
+ const sorted = Array.from(degreeMap.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10);
11414
+ const hubScoresTop10 = sorted.map(([notePath, degree]) => {
11415
+ const note = index.notes.get(notePath);
11416
+ return {
11417
+ entity: note?.title ?? notePath,
11418
+ degree
11419
+ };
11420
+ });
11421
+ return {
11422
+ avg_degree: avgDegree,
11423
+ max_degree: maxDegree,
11424
+ cluster_count: clusterCount,
11425
+ largest_cluster_size: largestClusterSize,
11426
+ hub_scores_top10: hubScoresTop10
11427
+ };
11428
+ }
11429
+ function recordGraphSnapshot(stateDb2, metrics) {
11430
+ const timestamp = Date.now();
11431
+ const insert = stateDb2.db.prepare(
11432
+ "INSERT INTO graph_snapshots (timestamp, metric, value, details) VALUES (?, ?, ?, ?)"
11433
+ );
11434
+ const transaction = stateDb2.db.transaction(() => {
11435
+ insert.run(timestamp, "avg_degree", metrics.avg_degree, null);
11436
+ insert.run(timestamp, "max_degree", metrics.max_degree, null);
11437
+ insert.run(timestamp, "cluster_count", metrics.cluster_count, null);
11438
+ insert.run(timestamp, "largest_cluster_size", metrics.largest_cluster_size, null);
11439
+ insert.run(
11440
+ timestamp,
11441
+ "hub_scores_top10",
11442
+ metrics.hub_scores_top10.length,
11443
+ JSON.stringify(metrics.hub_scores_top10)
11444
+ );
11445
+ });
11446
+ transaction();
11447
+ }
11448
+ function getEmergingHubs(stateDb2, daysBack = 30) {
11449
+ const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
11450
+ const latestRow = stateDb2.db.prepare(
11451
+ `SELECT details FROM graph_snapshots
11452
+ WHERE metric = 'hub_scores_top10'
11453
+ ORDER BY timestamp DESC LIMIT 1`
11454
+ ).get();
11455
+ const previousRow = stateDb2.db.prepare(
11456
+ `SELECT details FROM graph_snapshots
11457
+ WHERE metric = 'hub_scores_top10' AND timestamp >= ?
11458
+ ORDER BY timestamp ASC LIMIT 1`
11459
+ ).get(cutoff);
11460
+ if (!latestRow?.details) return [];
11461
+ const currentHubs = JSON.parse(latestRow.details);
11462
+ const previousHubs = previousRow?.details ? JSON.parse(previousRow.details) : [];
11463
+ const previousMap = /* @__PURE__ */ new Map();
11464
+ for (const hub of previousHubs) {
11465
+ previousMap.set(hub.entity, hub.degree);
11466
+ }
11467
+ const emerging = currentHubs.map((hub) => {
11468
+ const prevDegree = previousMap.get(hub.entity) ?? 0;
11469
+ return {
11470
+ entity: hub.entity,
11471
+ current_degree: hub.degree,
11472
+ previous_degree: prevDegree,
11473
+ growth: hub.degree - prevDegree
11474
+ };
11475
+ });
11476
+ emerging.sort((a, b) => b.growth - a.growth);
11477
+ return emerging;
11478
+ }
11479
+ function compareGraphSnapshots(stateDb2, timestampBefore, timestampAfter) {
11480
+ const SCALAR_METRICS = ["avg_degree", "max_degree", "cluster_count", "largest_cluster_size"];
11481
+ function getSnapshotAt(ts) {
11482
+ const row = stateDb2.db.prepare(
11483
+ `SELECT DISTINCT timestamp FROM graph_snapshots WHERE timestamp <= ? ORDER BY timestamp DESC LIMIT 1`
11484
+ ).get(ts);
11485
+ if (!row) return null;
11486
+ const rows = stateDb2.db.prepare(
11487
+ `SELECT metric, value, details FROM graph_snapshots WHERE timestamp = ?`
11488
+ ).all(row.timestamp);
11489
+ return rows;
11490
+ }
11491
+ const beforeRows = getSnapshotAt(timestampBefore) ?? [];
11492
+ const afterRows = getSnapshotAt(timestampAfter) ?? [];
11493
+ const beforeMap = /* @__PURE__ */ new Map();
11494
+ const afterMap = /* @__PURE__ */ new Map();
11495
+ for (const r of beforeRows) beforeMap.set(r.metric, { value: r.value, details: r.details });
11496
+ for (const r of afterRows) afterMap.set(r.metric, { value: r.value, details: r.details });
11497
+ const metricChanges = SCALAR_METRICS.map((metric) => {
11498
+ const before = beforeMap.get(metric)?.value ?? 0;
11499
+ const after = afterMap.get(metric)?.value ?? 0;
11500
+ const delta = after - before;
11501
+ const deltaPercent = before !== 0 ? Math.round(delta / before * 1e4) / 100 : delta !== 0 ? 100 : 0;
11502
+ return { metric, before, after, delta, deltaPercent };
11503
+ });
11504
+ const beforeHubs = beforeMap.get("hub_scores_top10")?.details ? JSON.parse(beforeMap.get("hub_scores_top10").details) : [];
11505
+ const afterHubs = afterMap.get("hub_scores_top10")?.details ? JSON.parse(afterMap.get("hub_scores_top10").details) : [];
11506
+ const beforeHubMap = /* @__PURE__ */ new Map();
11507
+ for (const h of beforeHubs) beforeHubMap.set(h.entity, h.degree);
11508
+ const afterHubMap = /* @__PURE__ */ new Map();
11509
+ for (const h of afterHubs) afterHubMap.set(h.entity, h.degree);
11510
+ const allHubEntities = /* @__PURE__ */ new Set([...beforeHubMap.keys(), ...afterHubMap.keys()]);
11511
+ const hubScoreChanges = [];
11512
+ for (const entity of allHubEntities) {
11513
+ const before = beforeHubMap.get(entity) ?? 0;
11514
+ const after = afterHubMap.get(entity) ?? 0;
11515
+ if (before !== after) {
11516
+ hubScoreChanges.push({ entity, before, after, delta: after - before });
11517
+ }
11518
+ }
11519
+ hubScoreChanges.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
11520
+ return { metricChanges, hubScoreChanges };
11521
+ }
11522
+ function purgeOldSnapshots(stateDb2, retentionDays = 90) {
11523
+ const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
11524
+ const result = stateDb2.db.prepare(
11525
+ "DELETE FROM graph_snapshots WHERE timestamp < ?"
11526
+ ).run(cutoff);
11527
+ return result.changes;
11528
+ }
11529
+
11530
+ // src/core/read/watch/maintenance.ts
11531
+ var DEFAULT_INTERVAL_MS = 2 * 60 * 60 * 1e3;
11532
+ var MIN_INTERVAL_MS = 10 * 60 * 1e3;
11533
+ var JITTER_FACTOR = 0.15;
11534
+ var RECENT_REBUILD_THRESHOLD_MS = 60 * 60 * 1e3;
11535
+ var IDLE_THRESHOLD_MS = 30 * 1e3;
11536
+ var STEP_TTLS = {
11537
+ entity_scan: 5 * 60 * 1e3,
11538
+ // 5 minutes
11539
+ hub_scores: 5 * 60 * 1e3,
11540
+ // 5 minutes
11541
+ recency: 60 * 60 * 1e3,
11542
+ // 1 hour
11543
+ cooccurrence: 60 * 60 * 1e3,
11544
+ // 1 hour
11545
+ edge_weights: 60 * 60 * 1e3,
11546
+ // 1 hour
11547
+ config_inference: 2 * 60 * 60 * 1e3
11548
+ // 2 hours (only runs during maintenance)
11549
+ };
11550
+ var timer = null;
11551
+ var config = null;
11552
+ var lastConfigInferenceAt = 0;
11553
+ function addJitter(interval) {
11554
+ const jitter = interval * JITTER_FACTOR * (2 * Math.random() - 1);
11555
+ return Math.max(MIN_INTERVAL_MS, interval + jitter);
11556
+ }
11557
+ function startMaintenanceTimer(cfg, intervalMs) {
11558
+ config = cfg;
11559
+ const baseInterval = Math.max(intervalMs ?? DEFAULT_INTERVAL_MS, MIN_INTERVAL_MS);
11560
+ scheduleNext(baseInterval);
11561
+ serverLog("maintenance", `Timer started (interval ~${Math.round(baseInterval / 6e4)}min)`);
11562
+ }
11563
+ function stopMaintenanceTimer() {
11564
+ if (timer) {
11565
+ clearTimeout(timer);
11566
+ timer = null;
11567
+ }
11568
+ config = null;
11569
+ }
11570
+ function scheduleNext(baseInterval) {
11571
+ timer = setTimeout(() => {
11572
+ runMaintenance(baseInterval);
11573
+ }, addJitter(baseInterval));
11574
+ timer.unref();
11575
+ }
11576
+ async function runMaintenance(baseInterval) {
11577
+ const cfg = config;
11578
+ if (!cfg) return;
11579
+ const { ctx, sd } = cfg;
11580
+ if (ctx.pipelineActivity.busy) {
11581
+ serverLog("maintenance", "Skipped: pipeline busy");
11582
+ scheduleNext(baseInterval);
11583
+ return;
11584
+ }
11585
+ const lastFullRebuild = cfg.getLastFullRebuildAt();
11586
+ if (lastFullRebuild > 0 && Date.now() - lastFullRebuild < RECENT_REBUILD_THRESHOLD_MS) {
11587
+ serverLog("maintenance", `Skipped: full rebuild ${Math.round((Date.now() - lastFullRebuild) / 6e4)}min ago`);
11588
+ scheduleNext(baseInterval);
11589
+ return;
11590
+ }
11591
+ const lastRequest = cfg.getLastMcpRequestAt();
11592
+ if (lastRequest > 0 && Date.now() - lastRequest < IDLE_THRESHOLD_MS) {
11593
+ serverLog("maintenance", "Skipped: server not idle, retrying in 1min");
11594
+ timer = setTimeout(() => runMaintenance(baseInterval), 60 * 1e3);
11595
+ timer.unref();
11596
+ return;
11597
+ }
11598
+ const start = Date.now();
11599
+ const stepsRun = [];
11600
+ const tracker = createStepTracker();
11601
+ try {
11602
+ const now = Date.now();
11603
+ const entityAge = ctx.lastEntityScanAt > 0 ? now - ctx.lastEntityScanAt : Infinity;
11604
+ if (entityAge >= STEP_TTLS.entity_scan) {
11605
+ tracker.start("entity_scan", {});
11606
+ await cfg.updateEntitiesInStateDb(cfg.vp, sd);
11607
+ ctx.lastEntityScanAt = Date.now();
11608
+ const entities = sd ? getAllEntitiesFromDb2(sd) : [];
11609
+ tracker.end({ entity_count: entities.length });
11610
+ stepsRun.push("entity_scan");
11611
+ }
11612
+ const hubAge = ctx.lastHubScoreRebuildAt > 0 ? now - ctx.lastHubScoreRebuildAt : Infinity;
11613
+ if (hubAge >= STEP_TTLS.hub_scores) {
11614
+ tracker.start("hub_scores", {});
11615
+ const updated = await exportHubScores(cfg.getVaultIndex(), sd);
11616
+ ctx.lastHubScoreRebuildAt = Date.now();
11617
+ tracker.end({ updated: updated ?? 0 });
11618
+ stepsRun.push("hub_scores");
11619
+ }
11620
+ const cachedRecency = loadRecencyFromStateDb(sd ?? void 0);
11621
+ const recencyAge = cachedRecency ? now - (cachedRecency.lastUpdated ?? 0) : Infinity;
11622
+ if (recencyAge >= STEP_TTLS.recency) {
11623
+ tracker.start("recency", {});
11624
+ const entities = sd ? getAllEntitiesFromDb2(sd) : [];
11625
+ const entityInput = entities.map((e) => ({ name: e.name, path: e.path, aliases: e.aliases }));
11626
+ const recencyIndex2 = await buildRecencyIndex(cfg.vp, entityInput);
11627
+ saveRecencyToStateDb(recencyIndex2, sd ?? void 0);
11628
+ tracker.end({ entities: recencyIndex2.lastMentioned.size });
11629
+ stepsRun.push("recency");
11630
+ }
11631
+ const cooccurrenceAge = ctx.lastCooccurrenceRebuildAt > 0 ? now - ctx.lastCooccurrenceRebuildAt : Infinity;
11632
+ if (cooccurrenceAge >= STEP_TTLS.cooccurrence) {
11633
+ tracker.start("cooccurrence", {});
11634
+ const entities = sd ? getAllEntitiesFromDb2(sd) : [];
11635
+ const entityNames = entities.map((e) => e.name);
11636
+ const cooccurrenceIdx = await mineCooccurrences(cfg.vp, entityNames);
11637
+ setCooccurrenceIndex(cooccurrenceIdx);
11638
+ ctx.lastCooccurrenceRebuildAt = Date.now();
11639
+ ctx.cooccurrenceIndex = cooccurrenceIdx;
11640
+ if (sd) saveCooccurrenceToStateDb(sd, cooccurrenceIdx);
11641
+ tracker.end({ associations: cooccurrenceIdx._metadata.total_associations });
11642
+ stepsRun.push("cooccurrence");
11643
+ }
11644
+ const edgeWeightAge = ctx.lastEdgeWeightRebuildAt > 0 ? now - ctx.lastEdgeWeightRebuildAt : Infinity;
11645
+ if (sd && edgeWeightAge >= STEP_TTLS.edge_weights) {
11646
+ tracker.start("edge_weights", {});
11647
+ const result = recomputeEdgeWeights(sd);
11648
+ ctx.lastEdgeWeightRebuildAt = Date.now();
11649
+ tracker.end({ edges: result.edges_updated });
11650
+ stepsRun.push("edge_weights");
11651
+ }
11652
+ const configAge = lastConfigInferenceAt > 0 ? now - lastConfigInferenceAt : Infinity;
11653
+ if (sd && configAge >= STEP_TTLS.config_inference) {
11654
+ tracker.start("config_inference", {});
11655
+ const existing = loadConfig(sd);
11656
+ const inferred = inferConfig(cfg.getVaultIndex(), cfg.vp);
11657
+ saveConfig(sd, inferred, existing);
11658
+ cfg.updateFlywheelConfig(loadConfig(sd));
11659
+ lastConfigInferenceAt = Date.now();
11660
+ tracker.end({ inferred: true });
11661
+ stepsRun.push("config_inference");
11662
+ }
11663
+ if (sd && stepsRun.length > 0) {
11664
+ try {
11665
+ tracker.start("graph_snapshot", {});
11666
+ const graphMetrics = computeGraphMetrics(cfg.getVaultIndex());
11667
+ recordGraphSnapshot(sd, graphMetrics);
11668
+ tracker.end({ recorded: true });
11669
+ stepsRun.push("graph_snapshot");
11670
+ } catch (err) {
11671
+ tracker.end({ error: String(err) });
11672
+ }
11673
+ }
11674
+ if (sd && stepsRun.length > 0) {
11675
+ try {
11676
+ saveVaultIndexToCache(sd, cfg.getVaultIndex());
11677
+ ctx.lastIndexCacheSaveAt = Date.now();
11678
+ } catch {
11679
+ }
11680
+ }
11681
+ const duration = Date.now() - start;
11682
+ if (stepsRun.length > 0) {
11683
+ serverLog("maintenance", `Completed ${stepsRun.length} steps in ${duration}ms: ${stepsRun.join(", ")}`);
11684
+ if (sd) {
11685
+ recordIndexEvent(sd, {
11686
+ trigger: "maintenance",
11687
+ duration_ms: duration,
11688
+ note_count: cfg.getVaultIndex().notes.size,
11689
+ steps: tracker.steps
11690
+ });
11691
+ }
11692
+ } else {
11693
+ serverLog("maintenance", `All steps fresh, nothing to do (${duration}ms)`);
11694
+ }
11695
+ } catch (err) {
11696
+ const duration = Date.now() - start;
11697
+ serverLog("maintenance", `Failed after ${duration}ms: ${err instanceof Error ? err.message : err}`, "error");
11698
+ if (sd) {
11699
+ recordIndexEvent(sd, {
11700
+ trigger: "maintenance",
11701
+ duration_ms: duration,
11702
+ success: false,
11703
+ error: err instanceof Error ? err.message : String(err),
11704
+ steps: tracker.steps
11705
+ });
11706
+ }
11707
+ }
11708
+ scheduleNext(baseInterval);
11709
+ }
11710
+
11232
11711
  // src/core/shared/metrics.ts
11233
11712
  init_wikilinkFeedback();
11234
11713
  var ALL_METRICS = [
@@ -11617,193 +12096,6 @@ function purgeOldInvocations(stateDb2, retentionDays = 90) {
11617
12096
  return result.changes;
11618
12097
  }
11619
12098
 
11620
- // src/core/shared/graphSnapshots.ts
11621
- function computeGraphMetrics(index) {
11622
- const noteCount = index.notes.size;
11623
- if (noteCount === 0) {
11624
- return {
11625
- avg_degree: 0,
11626
- max_degree: 0,
11627
- cluster_count: 0,
11628
- largest_cluster_size: 0,
11629
- hub_scores_top10: []
11630
- };
11631
- }
11632
- const degreeMap = /* @__PURE__ */ new Map();
11633
- const adjacency = /* @__PURE__ */ new Map();
11634
- for (const [notePath, note] of index.notes) {
11635
- if (!adjacency.has(notePath)) adjacency.set(notePath, /* @__PURE__ */ new Set());
11636
- let degree = note.outlinks.length;
11637
- for (const link of note.outlinks) {
11638
- const targetLower = link.target.toLowerCase();
11639
- const resolvedPath = index.entities.get(targetLower);
11640
- if (resolvedPath && index.notes.has(resolvedPath)) {
11641
- adjacency.get(notePath).add(resolvedPath);
11642
- if (!adjacency.has(resolvedPath)) adjacency.set(resolvedPath, /* @__PURE__ */ new Set());
11643
- adjacency.get(resolvedPath).add(notePath);
11644
- }
11645
- }
11646
- degreeMap.set(notePath, degree);
11647
- }
11648
- for (const [target, backlinks] of index.backlinks) {
11649
- const targetLower = target.toLowerCase();
11650
- const resolvedPath = index.entities.get(targetLower);
11651
- if (resolvedPath && degreeMap.has(resolvedPath)) {
11652
- degreeMap.set(resolvedPath, degreeMap.get(resolvedPath) + backlinks.length);
11653
- }
11654
- }
11655
- let totalDegree = 0;
11656
- let maxDegree = 0;
11657
- let maxDegreeNote = "";
11658
- for (const [notePath, degree] of degreeMap) {
11659
- totalDegree += degree;
11660
- if (degree > maxDegree) {
11661
- maxDegree = degree;
11662
- maxDegreeNote = notePath;
11663
- }
11664
- }
11665
- const avgDegree = noteCount > 0 ? Math.round(totalDegree / noteCount * 100) / 100 : 0;
11666
- const visited = /* @__PURE__ */ new Set();
11667
- const clusters = [];
11668
- for (const notePath of index.notes.keys()) {
11669
- if (visited.has(notePath)) continue;
11670
- const queue = [notePath];
11671
- visited.add(notePath);
11672
- let clusterSize = 0;
11673
- while (queue.length > 0) {
11674
- const current = queue.shift();
11675
- clusterSize++;
11676
- const neighbors = adjacency.get(current);
11677
- if (neighbors) {
11678
- for (const neighbor of neighbors) {
11679
- if (!visited.has(neighbor)) {
11680
- visited.add(neighbor);
11681
- queue.push(neighbor);
11682
- }
11683
- }
11684
- }
11685
- }
11686
- clusters.push(clusterSize);
11687
- }
11688
- const clusterCount = clusters.length;
11689
- const largestClusterSize = clusters.length > 0 ? Math.max(...clusters) : 0;
11690
- const sorted = Array.from(degreeMap.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10);
11691
- const hubScoresTop10 = sorted.map(([notePath, degree]) => {
11692
- const note = index.notes.get(notePath);
11693
- return {
11694
- entity: note?.title ?? notePath,
11695
- degree
11696
- };
11697
- });
11698
- return {
11699
- avg_degree: avgDegree,
11700
- max_degree: maxDegree,
11701
- cluster_count: clusterCount,
11702
- largest_cluster_size: largestClusterSize,
11703
- hub_scores_top10: hubScoresTop10
11704
- };
11705
- }
11706
- function recordGraphSnapshot(stateDb2, metrics) {
11707
- const timestamp = Date.now();
11708
- const insert = stateDb2.db.prepare(
11709
- "INSERT INTO graph_snapshots (timestamp, metric, value, details) VALUES (?, ?, ?, ?)"
11710
- );
11711
- const transaction = stateDb2.db.transaction(() => {
11712
- insert.run(timestamp, "avg_degree", metrics.avg_degree, null);
11713
- insert.run(timestamp, "max_degree", metrics.max_degree, null);
11714
- insert.run(timestamp, "cluster_count", metrics.cluster_count, null);
11715
- insert.run(timestamp, "largest_cluster_size", metrics.largest_cluster_size, null);
11716
- insert.run(
11717
- timestamp,
11718
- "hub_scores_top10",
11719
- metrics.hub_scores_top10.length,
11720
- JSON.stringify(metrics.hub_scores_top10)
11721
- );
11722
- });
11723
- transaction();
11724
- }
11725
- function getEmergingHubs(stateDb2, daysBack = 30) {
11726
- const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
11727
- const latestRow = stateDb2.db.prepare(
11728
- `SELECT details FROM graph_snapshots
11729
- WHERE metric = 'hub_scores_top10'
11730
- ORDER BY timestamp DESC LIMIT 1`
11731
- ).get();
11732
- const previousRow = stateDb2.db.prepare(
11733
- `SELECT details FROM graph_snapshots
11734
- WHERE metric = 'hub_scores_top10' AND timestamp >= ?
11735
- ORDER BY timestamp ASC LIMIT 1`
11736
- ).get(cutoff);
11737
- if (!latestRow?.details) return [];
11738
- const currentHubs = JSON.parse(latestRow.details);
11739
- const previousHubs = previousRow?.details ? JSON.parse(previousRow.details) : [];
11740
- const previousMap = /* @__PURE__ */ new Map();
11741
- for (const hub of previousHubs) {
11742
- previousMap.set(hub.entity, hub.degree);
11743
- }
11744
- const emerging = currentHubs.map((hub) => {
11745
- const prevDegree = previousMap.get(hub.entity) ?? 0;
11746
- return {
11747
- entity: hub.entity,
11748
- current_degree: hub.degree,
11749
- previous_degree: prevDegree,
11750
- growth: hub.degree - prevDegree
11751
- };
11752
- });
11753
- emerging.sort((a, b) => b.growth - a.growth);
11754
- return emerging;
11755
- }
11756
- function compareGraphSnapshots(stateDb2, timestampBefore, timestampAfter) {
11757
- const SCALAR_METRICS = ["avg_degree", "max_degree", "cluster_count", "largest_cluster_size"];
11758
- function getSnapshotAt(ts) {
11759
- const row = stateDb2.db.prepare(
11760
- `SELECT DISTINCT timestamp FROM graph_snapshots WHERE timestamp <= ? ORDER BY timestamp DESC LIMIT 1`
11761
- ).get(ts);
11762
- if (!row) return null;
11763
- const rows = stateDb2.db.prepare(
11764
- `SELECT metric, value, details FROM graph_snapshots WHERE timestamp = ?`
11765
- ).all(row.timestamp);
11766
- return rows;
11767
- }
11768
- const beforeRows = getSnapshotAt(timestampBefore) ?? [];
11769
- const afterRows = getSnapshotAt(timestampAfter) ?? [];
11770
- const beforeMap = /* @__PURE__ */ new Map();
11771
- const afterMap = /* @__PURE__ */ new Map();
11772
- for (const r of beforeRows) beforeMap.set(r.metric, { value: r.value, details: r.details });
11773
- for (const r of afterRows) afterMap.set(r.metric, { value: r.value, details: r.details });
11774
- const metricChanges = SCALAR_METRICS.map((metric) => {
11775
- const before = beforeMap.get(metric)?.value ?? 0;
11776
- const after = afterMap.get(metric)?.value ?? 0;
11777
- const delta = after - before;
11778
- const deltaPercent = before !== 0 ? Math.round(delta / before * 1e4) / 100 : delta !== 0 ? 100 : 0;
11779
- return { metric, before, after, delta, deltaPercent };
11780
- });
11781
- const beforeHubs = beforeMap.get("hub_scores_top10")?.details ? JSON.parse(beforeMap.get("hub_scores_top10").details) : [];
11782
- const afterHubs = afterMap.get("hub_scores_top10")?.details ? JSON.parse(afterMap.get("hub_scores_top10").details) : [];
11783
- const beforeHubMap = /* @__PURE__ */ new Map();
11784
- for (const h of beforeHubs) beforeHubMap.set(h.entity, h.degree);
11785
- const afterHubMap = /* @__PURE__ */ new Map();
11786
- for (const h of afterHubs) afterHubMap.set(h.entity, h.degree);
11787
- const allHubEntities = /* @__PURE__ */ new Set([...beforeHubMap.keys(), ...afterHubMap.keys()]);
11788
- const hubScoreChanges = [];
11789
- for (const entity of allHubEntities) {
11790
- const before = beforeHubMap.get(entity) ?? 0;
11791
- const after = afterHubMap.get(entity) ?? 0;
11792
- if (before !== after) {
11793
- hubScoreChanges.push({ entity, before, after, delta: after - before });
11794
- }
11795
- }
11796
- hubScoreChanges.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
11797
- return { metricChanges, hubScoreChanges };
11798
- }
11799
- function purgeOldSnapshots(stateDb2, retentionDays = 90) {
11800
- const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
11801
- const result = stateDb2.db.prepare(
11802
- "DELETE FROM graph_snapshots WHERE timestamp < ?"
11803
- ).run(cutoff);
11804
- return result.changes;
11805
- }
11806
-
11807
12099
  // src/index.ts
11808
12100
  init_serverLog();
11809
12101
  init_wikilinkFeedback();
@@ -12684,8 +12976,8 @@ var DEFAULT_CONFIG2 = {
12684
12976
  maxOutlinksPerHop: 10,
12685
12977
  maxBackfill: 10
12686
12978
  };
12687
- function multiHopBackfill(primaryResults, index, stateDb2, config = {}) {
12688
- const cfg = { ...DEFAULT_CONFIG2, ...config };
12979
+ function multiHopBackfill(primaryResults, index, stateDb2, config2 = {}) {
12980
+ const cfg = { ...DEFAULT_CONFIG2, ...config2 };
12689
12981
  const seen = new Set(primaryResults.map((r) => r.path).filter(Boolean));
12690
12982
  const candidates = [];
12691
12983
  const hop1Results = [];
@@ -15501,7 +15793,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
15501
15793
  }
15502
15794
  const indexBuilt = indexState2 === "ready" && index !== void 0 && index.notes !== void 0;
15503
15795
  let lastIndexActivityAt;
15504
- let lastFullRebuildAt;
15796
+ let lastFullRebuildAt2;
15505
15797
  let lastWatcherBatchAt;
15506
15798
  let lastBuild;
15507
15799
  let lastManual;
@@ -15511,13 +15803,13 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
15511
15803
  if (lastAny) lastIndexActivityAt = lastAny.timestamp;
15512
15804
  lastBuild = getLastEventByTrigger(stateDb2, "startup_build") ?? void 0;
15513
15805
  lastManual = getLastEventByTrigger(stateDb2, "manual_refresh") ?? void 0;
15514
- lastFullRebuildAt = Math.max(lastBuild?.timestamp ?? 0, lastManual?.timestamp ?? 0) || void 0;
15806
+ lastFullRebuildAt2 = Math.max(lastBuild?.timestamp ?? 0, lastManual?.timestamp ?? 0) || void 0;
15515
15807
  const lastWatcher = getLastEventByTrigger(stateDb2, "watcher");
15516
15808
  if (lastWatcher) lastWatcherBatchAt = lastWatcher.timestamp;
15517
15809
  } catch {
15518
15810
  }
15519
15811
  }
15520
- const freshnessTimestamp = lastFullRebuildAt ?? (indexBuilt && index.builtAt ? index.builtAt.getTime() : void 0);
15812
+ const freshnessTimestamp = lastFullRebuildAt2 ?? (indexBuilt && index.builtAt ? index.builtAt.getTime() : void 0);
15521
15813
  const indexAge = freshnessTimestamp ? Math.floor((Date.now() - freshnessTimestamp) / 1e3) : -1;
15522
15814
  const indexStale = indexBuilt && indexAge > STALE_THRESHOLD_SECONDS;
15523
15815
  if (indexState2 === "building") {
@@ -15564,8 +15856,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
15564
15856
  }
15565
15857
  let configInfo;
15566
15858
  if (isFull) {
15567
- const config = getConfig2();
15568
- configInfo = Object.keys(config).length > 0 ? config : void 0;
15859
+ const config2 = getConfig2();
15860
+ configInfo = Object.keys(config2).length > 0 ? config2 : void 0;
15569
15861
  }
15570
15862
  let lastRebuild;
15571
15863
  if (stateDb2) {
@@ -15694,15 +15986,15 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
15694
15986
  watcher_pending: getWatcherStatus2()?.pendingEvents,
15695
15987
  last_index_activity_at: lastIndexActivityAt,
15696
15988
  last_index_activity_ago_seconds: lastIndexActivityAt ? Math.floor((Date.now() - lastIndexActivityAt) / 1e3) : void 0,
15697
- last_full_rebuild_at: lastFullRebuildAt,
15989
+ last_full_rebuild_at: lastFullRebuildAt2,
15698
15990
  last_watcher_batch_at: lastWatcherBatchAt,
15699
15991
  pipeline_activity: pipelineActivity,
15700
15992
  dead_link_count: isFull ? deadLinkCount : void 0,
15701
15993
  top_dead_link_targets: isFull ? topDeadLinkTargets : void 0,
15702
15994
  sweep: isFull ? getSweepResults() ?? void 0 : void 0,
15703
15995
  proactive_linking: isFull && stateDb2 ? (() => {
15704
- const config = getConfig2();
15705
- const enabled = config.proactive_linking !== false;
15996
+ const config2 = getConfig2();
15997
+ const enabled = config2.proactive_linking !== false;
15706
15998
  const queuePending = stateDb2.db.prepare(
15707
15999
  `SELECT COUNT(*) as cnt FROM proactive_queue WHERE status = 'pending'`
15708
16000
  ).get();
@@ -16348,7 +16640,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
16348
16640
 
16349
16641
  // src/tools/read/system.ts
16350
16642
  import { z as z5 } from "zod";
16351
- import { scanVaultEntities as scanVaultEntities2, getEntityIndexFromDb as getEntityIndexFromDb2, getAllEntitiesFromDb as getAllEntitiesFromDb2 } from "@velvetmonkey/vault-core";
16643
+ import { scanVaultEntities as scanVaultEntities3, getEntityIndexFromDb as getEntityIndexFromDb2, getAllEntitiesFromDb as getAllEntitiesFromDb3 } from "@velvetmonkey/vault-core";
16352
16644
 
16353
16645
  // src/core/read/aliasSuggestions.ts
16354
16646
  import { STOPWORDS_EN as STOPWORDS_EN3 } from "@velvetmonkey/vault-core";
@@ -16465,11 +16757,11 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
16465
16757
  if (stateDb2) {
16466
16758
  tracker.start("entity_sync", {});
16467
16759
  try {
16468
- const config = loadConfig(stateDb2);
16469
- const excludeFolders = config.exclude_entity_folders?.length ? config.exclude_entity_folders : DEFAULT_ENTITY_EXCLUDE_FOLDERS;
16470
- const entityIndex2 = await scanVaultEntities2(vaultPath2, {
16760
+ const config2 = loadConfig(stateDb2);
16761
+ const excludeFolders = config2.exclude_entity_folders?.length ? config2.exclude_entity_folders : DEFAULT_ENTITY_EXCLUDE_FOLDERS;
16762
+ const entityIndex2 = await scanVaultEntities3(vaultPath2, {
16471
16763
  excludeFolders,
16472
- customCategories: config.custom_categories
16764
+ customCategories: config2.custom_categories
16473
16765
  });
16474
16766
  stateDb2.replaceAllEntities(entityIndex2);
16475
16767
  tracker.end({ entities: entityIndex2._metadata.total_entities });
@@ -16591,7 +16883,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
16591
16883
  if (stateDb2) {
16592
16884
  tracker.start("recency", {});
16593
16885
  try {
16594
- const entities = getAllEntitiesFromDb2(stateDb2).map((e) => ({
16886
+ const entities = getAllEntitiesFromDb3(stateDb2).map((e) => ({
16595
16887
  name: e.name,
16596
16888
  path: e.path,
16597
16889
  aliases: e.aliases
@@ -16610,7 +16902,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
16610
16902
  if (stateDb2) {
16611
16903
  tracker.start("cooccurrence", {});
16612
16904
  try {
16613
- const entityNames = getAllEntitiesFromDb2(stateDb2).map((e) => e.name);
16905
+ const entityNames = getAllEntitiesFromDb3(stateDb2).map((e) => e.name);
16614
16906
  const cooccurrenceIdx = await mineCooccurrences(vaultPath2, entityNames);
16615
16907
  setCooccurrenceIndex(cooccurrenceIdx);
16616
16908
  saveCooccurrenceToStateDb(stateDb2, cooccurrenceIdx);
@@ -16655,7 +16947,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
16655
16947
  if (stateDb2 && hasEntityEmbeddingsIndex()) {
16656
16948
  tracker.start("entity_embeddings", {});
16657
16949
  try {
16658
- const entities = getAllEntitiesFromDb2(stateDb2);
16950
+ const entities = getAllEntitiesFromDb3(stateDb2);
16659
16951
  if (entities.length > 0) {
16660
16952
  const entityMap = new Map(entities.map((e) => [e.name, {
16661
16953
  name: e.name,
@@ -17082,9 +17374,9 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17082
17374
  const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
17083
17375
  const index = getIndex();
17084
17376
  const vaultPath2 = getVaultPath();
17085
- const config = getConfig2();
17377
+ const config2 = getConfig2();
17086
17378
  if (path39) {
17087
- const result2 = await getTasksFromNote(index, path39, vaultPath2, getExcludeTags(config));
17379
+ const result2 = await getTasksFromNote(index, path39, vaultPath2, getExcludeTags(config2));
17088
17380
  if (!result2) {
17089
17381
  return {
17090
17382
  content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path39 }, null, 2) }]
@@ -17107,12 +17399,12 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17107
17399
  };
17108
17400
  }
17109
17401
  if (isTaskCacheReady()) {
17110
- refreshIfStale(vaultPath2, index, getExcludeTags(config));
17402
+ refreshIfStale(vaultPath2, index, getExcludeTags(config2));
17111
17403
  if (has_due_date) {
17112
17404
  const result3 = queryTasksFromCache({
17113
17405
  status,
17114
17406
  folder,
17115
- excludeTags: getExcludeTags(config),
17407
+ excludeTags: getExcludeTags(config2),
17116
17408
  has_due_date: true,
17117
17409
  limit,
17118
17410
  offset
@@ -17129,7 +17421,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17129
17421
  status,
17130
17422
  folder,
17131
17423
  tag,
17132
- excludeTags: getExcludeTags(config),
17424
+ excludeTags: getExcludeTags(config2),
17133
17425
  limit,
17134
17426
  offset
17135
17427
  });
@@ -17148,7 +17440,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17148
17440
  const allResults = await getTasksWithDueDates(index, vaultPath2, {
17149
17441
  status,
17150
17442
  folder,
17151
- excludeTags: getExcludeTags(config)
17443
+ excludeTags: getExcludeTags(config2)
17152
17444
  });
17153
17445
  const paged2 = allResults.slice(offset, offset + limit);
17154
17446
  return {
@@ -17164,7 +17456,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17164
17456
  folder,
17165
17457
  tag,
17166
17458
  limit: limit + offset,
17167
- excludeTags: getExcludeTags(config)
17459
+ excludeTags: getExcludeTags(config2)
17168
17460
  });
17169
17461
  const paged = result.tasks.slice(offset, offset + limit);
17170
17462
  return {
@@ -17831,10 +18123,10 @@ function isTemplatePath(notePath) {
17831
18123
  const folder = notePath.split("/")[0]?.toLowerCase() || "";
17832
18124
  return folder === "templates" || folder === "template";
17833
18125
  }
17834
- function getExcludedPaths(index, config) {
18126
+ function getExcludedPaths(index, config2) {
17835
18127
  const excluded = /* @__PURE__ */ new Set();
17836
- const excludeTags = new Set(getExcludeTags(config).map((t) => t.toLowerCase()));
17837
- const excludeEntities = new Set(getExcludeEntities(config).map((e) => e.toLowerCase()));
18128
+ const excludeTags = new Set(getExcludeTags(config2).map((t) => t.toLowerCase()));
18129
+ const excludeEntities = new Set(getExcludeEntities(config2).map((e) => e.toLowerCase()));
17838
18130
  if (excludeTags.size === 0 && excludeEntities.size === 0) return excluded;
17839
18131
  for (const note of index.notes.values()) {
17840
18132
  if (excludeTags.size > 0) {
@@ -17881,8 +18173,8 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb4
17881
18173
  requireIndex();
17882
18174
  const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
17883
18175
  const index = getIndex();
17884
- const config = getConfig2?.() ?? {};
17885
- const excludedPaths = getExcludedPaths(index, config);
18176
+ const config2 = getConfig2?.() ?? {};
18177
+ const excludedPaths = getExcludedPaths(index, config2);
17886
18178
  switch (analysis) {
17887
18179
  case "orphans": {
17888
18180
  const allOrphans = findOrphanNotes(index, folder).filter((o) => !isPeriodicNote(o.path) && !excludedPaths.has(o.path));
@@ -18050,7 +18342,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb4
18050
18342
  return !note || !excludedPaths.has(note.path);
18051
18343
  });
18052
18344
  }
18053
- const excludeEntities = new Set(getExcludeEntities(config).map((e) => e.toLowerCase()));
18345
+ const excludeEntities = new Set(getExcludeEntities(config2).map((e) => e.toLowerCase()));
18054
18346
  if (excludeEntities.size > 0) {
18055
18347
  hubs = hubs.filter((hub) => !excludeEntities.has(hub.entity.toLowerCase()));
18056
18348
  }
@@ -19689,14 +19981,14 @@ async function executeDeleteNote(options) {
19689
19981
  }
19690
19982
 
19691
19983
  // src/tools/write/mutations.ts
19692
- async function createNoteFromTemplate(vaultPath2, notePath, config) {
19984
+ async function createNoteFromTemplate(vaultPath2, notePath, config2) {
19693
19985
  const validation = await validatePathSecure(vaultPath2, notePath);
19694
19986
  if (!validation.valid) {
19695
19987
  throw new Error(`Path blocked: ${validation.reason}`);
19696
19988
  }
19697
19989
  const fullPath = path25.join(vaultPath2, notePath);
19698
19990
  await fs23.mkdir(path25.dirname(fullPath), { recursive: true });
19699
- const templates = config.templates || {};
19991
+ const templates = config2.templates || {};
19700
19992
  const filename = path25.basename(notePath, ".md").toLowerCase();
19701
19993
  let templatePath;
19702
19994
  let periodicType;
@@ -19809,8 +20101,8 @@ function registerMutationTools(server2, getVaultPath, getConfig2 = () => ({})) {
19809
20101
  try {
19810
20102
  await fs23.access(fullPath);
19811
20103
  } catch {
19812
- const config = getConfig2();
19813
- const result = await createNoteFromTemplate(vaultPath2, notePath, config);
20104
+ const config2 = getConfig2();
20105
+ const result = await createNoteFromTemplate(vaultPath2, notePath, config2);
19814
20106
  noteCreated = result.created;
19815
20107
  templateUsed = result.templateUsed;
19816
20108
  }
@@ -23916,9 +24208,9 @@ function registerConfigTools(server2, getConfig2, setConfig, getStateDb4) {
23916
24208
  async ({ mode, key, value }) => {
23917
24209
  switch (mode) {
23918
24210
  case "get": {
23919
- const config = getConfig2();
24211
+ const config2 = getConfig2();
23920
24212
  return {
23921
- content: [{ type: "text", text: JSON.stringify(config, null, 2) }]
24213
+ content: [{ type: "text", text: JSON.stringify(config2, null, 2) }]
23922
24214
  };
23923
24215
  }
23924
24216
  case "set": {
@@ -23969,7 +24261,7 @@ init_wikilinkFeedback();
23969
24261
  import { z as z28 } from "zod";
23970
24262
  import * as fs32 from "fs/promises";
23971
24263
  import * as path35 from "path";
23972
- import { scanVaultEntities as scanVaultEntities3, SCHEMA_VERSION as SCHEMA_VERSION2 } from "@velvetmonkey/vault-core";
24264
+ import { scanVaultEntities as scanVaultEntities4, SCHEMA_VERSION as SCHEMA_VERSION2 } from "@velvetmonkey/vault-core";
23973
24265
  init_embeddings();
23974
24266
  function hasSkipWikilinks(content) {
23975
24267
  if (!content.startsWith("---")) return false;
@@ -24087,8 +24379,8 @@ async function executeRun(stateDb2, vaultPath2) {
24087
24379
  if (entityCount === 0) {
24088
24380
  const start = Date.now();
24089
24381
  try {
24090
- const config = loadConfig(stateDb2);
24091
- const entityIndex2 = await scanVaultEntities3(vaultPath2, { excludeFolders: EXCLUDE_FOLDERS, customCategories: config.custom_categories });
24382
+ const config2 = loadConfig(stateDb2);
24383
+ const entityIndex2 = await scanVaultEntities4(vaultPath2, { excludeFolders: EXCLUDE_FOLDERS, customCategories: config2.custom_categories });
24092
24384
  stateDb2.replaceAllEntities(entityIndex2);
24093
24385
  const newCount = entityIndex2._metadata.total_entities;
24094
24386
  steps.push({
@@ -24669,7 +24961,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb4) {
24669
24961
  // src/tools/read/semantic.ts
24670
24962
  init_embeddings();
24671
24963
  import { z as z31 } from "zod";
24672
- import { getAllEntitiesFromDb as getAllEntitiesFromDb3 } from "@velvetmonkey/vault-core";
24964
+ import { getAllEntitiesFromDb as getAllEntitiesFromDb4 } from "@velvetmonkey/vault-core";
24673
24965
  function registerSemanticTools(server2, getVaultPath, getStateDb4) {
24674
24966
  server2.registerTool(
24675
24967
  "init_semantic",
@@ -24734,7 +25026,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb4) {
24734
25026
  const embedded = progress.total - progress.skipped;
24735
25027
  let entityEmbedded = 0;
24736
25028
  try {
24737
- const allEntities = getAllEntitiesFromDb3(stateDb2);
25029
+ const allEntities = getAllEntitiesFromDb4(stateDb2);
24738
25030
  const entityMap = /* @__PURE__ */ new Map();
24739
25031
  for (const e of allEntities) {
24740
25032
  entityMap.set(e.name, {
@@ -24796,7 +25088,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb4) {
24796
25088
  // src/tools/read/merges.ts
24797
25089
  init_levenshtein();
24798
25090
  import { z as z32 } from "zod";
24799
- import { getAllEntitiesFromDb as getAllEntitiesFromDb4, getDismissedMergePairs, recordMergeDismissal } from "@velvetmonkey/vault-core";
25091
+ import { getAllEntitiesFromDb as getAllEntitiesFromDb5, getDismissedMergePairs, recordMergeDismissal } from "@velvetmonkey/vault-core";
24800
25092
  function normalizeName(name) {
24801
25093
  return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
24802
25094
  }
@@ -24814,7 +25106,7 @@ function registerMergeTools2(server2, getStateDb4) {
24814
25106
  content: [{ type: "text", text: JSON.stringify({ suggestions: [], error: "StateDb not available" }) }]
24815
25107
  };
24816
25108
  }
24817
- const entities = getAllEntitiesFromDb4(stateDb2);
25109
+ const entities = getAllEntitiesFromDb5(stateDb2);
24818
25110
  if (entities.length === 0) {
24819
25111
  return {
24820
25112
  content: [{ type: "text", text: JSON.stringify({ suggestions: [] }) }]
@@ -26297,7 +26589,7 @@ function queryFlywheelAgeDays(stateDb2) {
26297
26589
  if (!row?.first_ts) return 0;
26298
26590
  return Math.floor((Date.now() - row.first_ts) / (24 * 60 * 60 * 1e3));
26299
26591
  }
26300
- function getCalibrationExport(stateDb2, metrics, config, daysBack = 30, includeVaultId = true) {
26592
+ function getCalibrationExport(stateDb2, metrics, config2, daysBack = 30, includeVaultId = true) {
26301
26593
  const now = /* @__PURE__ */ new Date();
26302
26594
  const start = new Date(now);
26303
26595
  start.setDate(start.getDate() - daysBack + 1);
@@ -26320,8 +26612,8 @@ function getCalibrationExport(stateDb2, metrics, config, daysBack = 30, includeV
26320
26612
  connected_ratio: round(metrics.connected_ratio),
26321
26613
  semantic_enabled: hasEmbeddingsIndex(),
26322
26614
  flywheel_age_days: queryFlywheelAgeDays(stateDb2),
26323
- strictness_mode: config.wikilink_strictness ?? "balanced",
26324
- adaptive_strictness: config.adaptive_strictness ?? true
26615
+ strictness_mode: config2.wikilink_strictness ?? "balanced",
26616
+ adaptive_strictness: config2.adaptive_strictness ?? true
26325
26617
  },
26326
26618
  entity_distribution: queryEntityDistribution(stateDb2),
26327
26619
  funnel: queryFunnel2(stateDb2, startMs, startIso, endIso),
@@ -26355,11 +26647,11 @@ function registerCalibrationExportTools(server2, getIndex, getStateDb4, getConfi
26355
26647
  }
26356
26648
  const index = getIndex();
26357
26649
  const metrics = computeMetrics(index, stateDb2);
26358
- const config = getConfig2();
26650
+ const config2 = getConfig2();
26359
26651
  const report = getCalibrationExport(
26360
26652
  stateDb2,
26361
26653
  metrics,
26362
- config,
26654
+ config2,
26363
26655
  args.days_back ?? 30,
26364
26656
  args.include_vault_id ?? true
26365
26657
  );
@@ -26613,7 +26905,7 @@ function extractSearchMethod(result) {
26613
26905
  }
26614
26906
  return void 0;
26615
26907
  }
26616
- function applyToolGating(targetServer, categories, getDb4, registry, getVaultPath, vaultCallbacks, tierMode = "off", onTierStateChange, isFullToolset = false) {
26908
+ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPath, vaultCallbacks, tierMode = "off", onTierStateChange, isFullToolset = false, onToolCall) {
26617
26909
  let _registered = 0;
26618
26910
  let _skipped = 0;
26619
26911
  let tierOverride = "auto";
@@ -26706,6 +26998,7 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
26706
26998
  }
26707
26999
  function wrapWithTracking(toolName, handler) {
26708
27000
  return async (...args) => {
27001
+ onToolCall?.();
26709
27002
  const start = Date.now();
26710
27003
  let success = true;
26711
27004
  let notePaths;
@@ -27125,6 +27418,9 @@ var httpListener = null;
27125
27418
  var watchdogTimer = null;
27126
27419
  var serverReady = false;
27127
27420
  var shutdownRequested = false;
27421
+ var lastMcpRequestAt = 0;
27422
+ var lastFullRebuildAt = 0;
27423
+ var deferredScheduler = null;
27128
27424
  function getWatcherStatus() {
27129
27425
  if (vaultRegistry) {
27130
27426
  const name = globalThis.__flywheel_active_vault;
@@ -27158,8 +27454,8 @@ function handleTierStateChange(controller) {
27158
27454
  syncRuntimeTierState(controller);
27159
27455
  invalidateHttpPool();
27160
27456
  }
27161
- function getConfigToolTierOverride(config) {
27162
- return config.tool_tier_override ?? "auto";
27457
+ function getConfigToolTierOverride(config2) {
27458
+ return config2.tool_tier_override ?? "auto";
27163
27459
  }
27164
27460
  function buildRegistryContext() {
27165
27461
  return {
@@ -27191,7 +27487,10 @@ function createConfiguredServer() {
27191
27487
  buildVaultCallbacks(),
27192
27488
  toolTierMode,
27193
27489
  handleTierStateChange,
27194
- toolConfig.isFullToolset
27490
+ toolConfig.isFullToolset,
27491
+ () => {
27492
+ lastMcpRequestAt = Date.now();
27493
+ }
27195
27494
  );
27196
27495
  registerAllTools(s, ctx, toolTierController);
27197
27496
  toolTierController.setOverride(runtimeToolTierOverride);
@@ -27245,7 +27544,10 @@ var _gatingResult = applyToolGating(
27245
27544
  buildVaultCallbacks(),
27246
27545
  toolTierMode,
27247
27546
  handleTierStateChange,
27248
- toolConfig.isFullToolset
27547
+ toolConfig.isFullToolset,
27548
+ () => {
27549
+ lastMcpRequestAt = Date.now();
27550
+ }
27249
27551
  );
27250
27552
  registerAllTools(server, _registryCtx, _gatingResult);
27251
27553
  _gatingResult.setOverride(runtimeToolTierOverride);
@@ -27368,17 +27670,17 @@ function updateVaultIndex(index) {
27368
27670
  const ctx = getActiveVaultContext();
27369
27671
  if (ctx) ctx.vaultIndex = index;
27370
27672
  }
27371
- function updateFlywheelConfig(config) {
27372
- flywheelConfig = config;
27373
- setWikilinkConfig(config);
27673
+ function updateFlywheelConfig(config2) {
27674
+ flywheelConfig = config2;
27675
+ setWikilinkConfig(config2);
27374
27676
  if (toolTierMode === "tiered" && primaryToolTierController) {
27375
- primaryToolTierController.setOverride(getConfigToolTierOverride(config));
27677
+ primaryToolTierController.setOverride(getConfigToolTierOverride(config2));
27376
27678
  syncRuntimeTierState(primaryToolTierController);
27377
27679
  invalidateHttpPool();
27378
27680
  }
27379
27681
  const ctx = getActiveVaultContext();
27380
27682
  if (ctx) {
27381
- ctx.flywheelConfig = config;
27683
+ ctx.flywheelConfig = config2;
27382
27684
  setActiveScope(buildVaultScope(ctx));
27383
27685
  }
27384
27686
  }
@@ -27433,6 +27735,7 @@ async function bootVault(ctx, startTime) {
27433
27735
  note_count: cachedIndex.notes.size
27434
27736
  });
27435
27737
  }
27738
+ lastFullRebuildAt = Date.now();
27436
27739
  await runPostIndexWork(ctx);
27437
27740
  } else {
27438
27741
  serverLog("index", `[${ctx.name}] Cache miss: building from scratch`);
@@ -27457,6 +27760,7 @@ async function bootVault(ctx, startTime) {
27457
27760
  serverLog("index", `[${ctx.name}] Failed to save index cache: ${err instanceof Error ? err.message : err}`, "error");
27458
27761
  }
27459
27762
  }
27763
+ lastFullRebuildAt = Date.now();
27460
27764
  await runPostIndexWork(ctx);
27461
27765
  } catch (err) {
27462
27766
  updateIndexState("error", err instanceof Error ? err : new Error(String(err)));
@@ -27677,11 +27981,11 @@ async function updateEntitiesInStateDb(vp, sd) {
27677
27981
  const vault = vp ?? vaultPath;
27678
27982
  if (!db4) return;
27679
27983
  try {
27680
- const config = loadConfig(db4);
27681
- const excludeFolders = config.exclude_entity_folders?.length ? config.exclude_entity_folders : DEFAULT_ENTITY_EXCLUDE_FOLDERS;
27682
- const entityIndex2 = await scanVaultEntities4(vault, {
27984
+ const config2 = loadConfig(db4);
27985
+ const excludeFolders = config2.exclude_entity_folders?.length ? config2.exclude_entity_folders : DEFAULT_ENTITY_EXCLUDE_FOLDERS;
27986
+ const entityIndex2 = await scanVaultEntities5(vault, {
27683
27987
  excludeFolders,
27684
- customCategories: config.custom_categories
27988
+ customCategories: config2.custom_categories
27685
27989
  });
27686
27990
  db4.replaceAllEntities(entityIndex2);
27687
27991
  serverLog("index", `Updated ${entityIndex2._metadata.total_entities} entities in StateDb`);
@@ -27828,7 +28132,7 @@ async function runPostIndexWork(ctx) {
27828
28132
  serverLog("semantic", "Embeddings up-to-date, skipping build");
27829
28133
  loadEntityEmbeddingsToMemory();
27830
28134
  if (sd) {
27831
- const entities = getAllEntitiesFromDb5(sd);
28135
+ const entities = getAllEntitiesFromDb6(sd);
27832
28136
  if (entities.length > 0) {
27833
28137
  saveInferredCategories(classifyUncategorizedEntities(
27834
28138
  entities.map((entity) => ({
@@ -27865,7 +28169,7 @@ async function runPostIndexWork(ctx) {
27865
28169
  }
27866
28170
  });
27867
28171
  if (sd) {
27868
- const entities = getAllEntitiesFromDb5(sd);
28172
+ const entities = getAllEntitiesFromDb6(sd);
27869
28173
  if (entities.length > 0) {
27870
28174
  const entityMap = new Map(entities.map((e) => [e.name, {
27871
28175
  name: e.name,
@@ -27880,7 +28184,7 @@ async function runPostIndexWork(ctx) {
27880
28184
  activateVault(ctx);
27881
28185
  loadEntityEmbeddingsToMemory();
27882
28186
  if (sd) {
27883
- const entities = getAllEntitiesFromDb5(sd);
28187
+ const entities = getAllEntitiesFromDb6(sd);
27884
28188
  if (entities.length > 0) {
27885
28189
  saveInferredCategories(classifyUncategorizedEntities(
27886
28190
  entities.map((entity) => ({
@@ -27918,7 +28222,7 @@ async function runPostIndexWork(ctx) {
27918
28222
  serverLog("semantic", "Skipping \u2014 FLYWHEEL_SKIP_EMBEDDINGS");
27919
28223
  }
27920
28224
  if (process.env.FLYWHEEL_WATCH !== "false") {
27921
- const config = parseWatcherConfig();
28225
+ const config2 = parseWatcherConfig();
27922
28226
  const lastContentHashes = /* @__PURE__ */ new Map();
27923
28227
  if (sd) {
27924
28228
  const persisted = loadContentHashes(sd);
@@ -27927,7 +28231,15 @@ async function runPostIndexWork(ctx) {
27927
28231
  serverLog("watcher", `Loaded ${persisted.size} persisted content hashes`);
27928
28232
  }
27929
28233
  }
27930
- serverLog("watcher", `File watcher enabled (debounce: ${config.debounceMs}ms)`);
28234
+ serverLog("watcher", `File watcher enabled (debounce: ${config2.debounceMs}ms)`);
28235
+ deferredScheduler = new DeferredStepScheduler();
28236
+ deferredScheduler.setExecutor({
28237
+ ctx,
28238
+ vp,
28239
+ sd,
28240
+ getVaultIndex: () => vaultIndex,
28241
+ updateEntitiesInStateDb
28242
+ });
27931
28243
  const handleBatch = async (batch) => {
27932
28244
  const vaultPrefixes = /* @__PURE__ */ new Set([
27933
28245
  normalizePath(vp),
@@ -28068,13 +28380,14 @@ async function runPostIndexWork(ctx) {
28068
28380
  updateVaultIndex,
28069
28381
  updateEntitiesInStateDb,
28070
28382
  getVaultIndex: () => vaultIndex,
28071
- buildVaultIndex
28383
+ buildVaultIndex,
28384
+ deferredScheduler: deferredScheduler ?? void 0
28072
28385
  });
28073
28386
  await runner.run();
28074
28387
  };
28075
28388
  const watcher = createVaultWatcher({
28076
28389
  vaultPath: vp,
28077
- config,
28390
+ config: config2,
28078
28391
  onBatch: handleBatch,
28079
28392
  onStateChange: (status) => {
28080
28393
  if (status.state === "dirty") {
@@ -28115,6 +28428,18 @@ async function runPostIndexWork(ctx) {
28115
28428
  if (sd) runPeriodicMaintenance(sd);
28116
28429
  });
28117
28430
  serverLog("server", "Sweep timer started (5 min interval)");
28431
+ const maintenanceIntervalMs = parseInt(process.env.FLYWHEEL_MAINTENANCE_INTERVAL_MINUTES ?? "120", 10) * 60 * 1e3;
28432
+ startMaintenanceTimer({
28433
+ ctx,
28434
+ vp,
28435
+ sd,
28436
+ getVaultIndex: () => ctx.vaultIndex,
28437
+ updateEntitiesInStateDb,
28438
+ updateFlywheelConfig,
28439
+ getLastMcpRequestAt: () => lastMcpRequestAt,
28440
+ getLastFullRebuildAt: () => lastFullRebuildAt
28441
+ }, maintenanceIntervalMs);
28442
+ serverLog("server", `Maintenance timer started (~${Math.round(maintenanceIntervalMs / 6e4)}min interval)`);
28118
28443
  }
28119
28444
  const postDuration = Date.now() - postStart;
28120
28445
  serverLog("server", `Post-index work complete in ${postDuration}ms`);
@@ -28162,7 +28487,12 @@ function gracefulShutdown(signal) {
28162
28487
  watcherInstance?.stop();
28163
28488
  } catch {
28164
28489
  }
28490
+ try {
28491
+ deferredScheduler?.cancelAll();
28492
+ } catch {
28493
+ }
28165
28494
  stopSweepTimer();
28495
+ stopMaintenanceTimer();
28166
28496
  flushLogs().catch(() => {
28167
28497
  }).finally(() => process.exit(0));
28168
28498
  setTimeout(() => process.exit(0), 2e3).unref();