@velvetmonkey/flywheel-memory 2.5.8 → 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 +657 -316
  2. package/package.json +2 -2
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();
@@ -5283,6 +5283,9 @@ function findSection(content, sectionName) {
5283
5283
  contentStartLine
5284
5284
  };
5285
5285
  }
5286
+ function isCodeFenceLine(line) {
5287
+ return line.trim().startsWith("```");
5288
+ }
5286
5289
  function isPreformattedList(content) {
5287
5290
  const trimmed = content.trim();
5288
5291
  if (!trimmed) return false;
@@ -5350,9 +5353,11 @@ function formatContent(content, format) {
5350
5353
  }
5351
5354
  const sanitized = sanitizeForList(trimmed);
5352
5355
  const lines = sanitized.split("\n");
5356
+ let inCodeBlock = false;
5353
5357
  return lines.map((line, i) => {
5358
+ if (isCodeFenceLine(line)) inCodeBlock = !inCodeBlock;
5354
5359
  if (i === 0) return `- ${line}`;
5355
- if (line === "") return " ";
5360
+ if (line === "") return inCodeBlock ? " " : " <!-- -->";
5356
5361
  return ` ${line}`;
5357
5362
  }).join("\n");
5358
5363
  }
@@ -5361,9 +5366,11 @@ function formatContent(content, format) {
5361
5366
  return trimmed;
5362
5367
  }
5363
5368
  const lines = trimmed.split("\n");
5369
+ let inCodeBlock = false;
5364
5370
  return lines.map((line, i) => {
5371
+ if (isCodeFenceLine(line)) inCodeBlock = !inCodeBlock;
5365
5372
  if (i === 0) return `- [ ] ${line}`;
5366
- if (line === "") return " ";
5373
+ if (line === "") return inCodeBlock ? " " : " <!-- -->";
5367
5374
  return ` ${line}`;
5368
5375
  }).join("\n");
5369
5376
  }
@@ -5372,9 +5379,11 @@ function formatContent(content, format) {
5372
5379
  return trimmed;
5373
5380
  }
5374
5381
  const lines = trimmed.split("\n");
5382
+ let inCodeBlock = false;
5375
5383
  return lines.map((line, i) => {
5384
+ if (isCodeFenceLine(line)) inCodeBlock = !inCodeBlock;
5376
5385
  if (i === 0) return `1. ${line}`;
5377
- if (line === "") return " ";
5386
+ if (line === "") return inCodeBlock ? " " : " <!-- -->";
5378
5387
  return ` ${line}`;
5379
5388
  }).join("\n");
5380
5389
  }
@@ -5389,9 +5398,11 @@ function formatContent(content, format) {
5389
5398
  const prefix = `- **${hours}:${minutes}** `;
5390
5399
  const lines = sanitized.split("\n");
5391
5400
  const indent = " ";
5401
+ let inCodeBlock = false;
5392
5402
  return lines.map((line, i) => {
5403
+ if (isCodeFenceLine(line)) inCodeBlock = !inCodeBlock;
5393
5404
  if (i === 0) return `${prefix}${line}`;
5394
- if (line === "") return indent;
5405
+ if (line === "") return inCodeBlock ? indent : `${indent}<!-- -->`;
5395
5406
  return `${indent}${line}`;
5396
5407
  }).join("\n");
5397
5408
  }
@@ -7364,21 +7375,21 @@ var DEFAULT_CONFIG = {
7364
7375
  implicit_detection: true,
7365
7376
  adaptive_strictness: true
7366
7377
  };
7367
- function migrateExcludeConfig(config) {
7378
+ function migrateExcludeConfig(config2) {
7368
7379
  const oldTags = [
7369
- ...config.exclude_task_tags ?? [],
7370
- ...config.exclude_analysis_tags ?? []
7380
+ ...config2.exclude_task_tags ?? [],
7381
+ ...config2.exclude_analysis_tags ?? []
7371
7382
  ];
7372
- const oldEntities = config.exclude_entities ?? [];
7373
- 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;
7374
7385
  const normalizedTags = oldTags.map((t) => t.startsWith("#") ? t : `#${t}`);
7375
7386
  const merged = /* @__PURE__ */ new Set([
7376
- ...config.exclude ?? [],
7387
+ ...config2.exclude ?? [],
7377
7388
  ...normalizedTags,
7378
7389
  ...oldEntities
7379
7390
  ]);
7380
7391
  return {
7381
- ...config,
7392
+ ...config2,
7382
7393
  exclude: Array.from(merged),
7383
7394
  // Clear deprecated fields
7384
7395
  exclude_task_tags: void 0,
@@ -7477,11 +7488,11 @@ function inferConfig(index, vaultPath2) {
7477
7488
  }
7478
7489
  return inferred;
7479
7490
  }
7480
- function getExcludeTags(config) {
7481
- 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));
7482
7493
  }
7483
- function getExcludeEntities(config) {
7484
- return (config.exclude ?? []).filter((e) => !e.startsWith("#"));
7494
+ function getExcludeEntities(config2) {
7495
+ return (config2.exclude ?? []).filter((e) => !e.startsWith("#"));
7485
7496
  }
7486
7497
  var TEMPLATE_PATTERNS = {
7487
7498
  daily: /^daily[\s._-]*(note|template)?\.md$/i,
@@ -7801,8 +7812,8 @@ var EventQueue = class {
7801
7812
  config;
7802
7813
  flushTimer = null;
7803
7814
  onBatch;
7804
- constructor(config, onBatch) {
7805
- this.config = config;
7815
+ constructor(config2, onBatch) {
7816
+ this.config = config2;
7806
7817
  this.onBatch = onBatch;
7807
7818
  }
7808
7819
  /**
@@ -8215,7 +8226,7 @@ init_serverLog();
8215
8226
  // src/core/read/watch/index.ts
8216
8227
  function createVaultWatcher(options) {
8217
8228
  const { vaultPath: vaultPath2, onBatch, onStateChange, onError } = options;
8218
- const config = {
8229
+ const config2 = {
8219
8230
  ...DEFAULT_WATCHER_CONFIG,
8220
8231
  ...parseWatcherConfig(),
8221
8232
  ...options.config
@@ -8260,7 +8271,7 @@ function createVaultWatcher(options) {
8260
8271
  }
8261
8272
  }
8262
8273
  };
8263
- const eventQueue = new EventQueue(config, processBatch2);
8274
+ const eventQueue = new EventQueue(config2, processBatch2);
8264
8275
  const instance = {
8265
8276
  get status() {
8266
8277
  return getStatus();
@@ -8273,8 +8284,8 @@ function createVaultWatcher(options) {
8273
8284
  console.error("[flywheel] Watcher already started");
8274
8285
  return;
8275
8286
  }
8276
- console.error(`[flywheel] Starting file watcher (debounce: ${config.debounceMs}ms, flush: ${config.flushMs}ms)`);
8277
- 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}`);
8278
8289
  watcher = chokidar.watch(vaultPath2, {
8279
8290
  ignored: createIgnoreFunction(vaultPath2),
8280
8291
  persistent: true,
@@ -8283,8 +8294,8 @@ function createVaultWatcher(options) {
8283
8294
  stabilityThreshold: 300,
8284
8295
  pollInterval: 100
8285
8296
  },
8286
- usePolling: config.usePolling,
8287
- interval: config.usePolling ? config.pollInterval : void 0
8297
+ usePolling: config2.usePolling,
8298
+ interval: config2.usePolling ? config2.pollInterval : void 0
8288
8299
  });
8289
8300
  watcher.on("add", (path39) => {
8290
8301
  console.error(`[flywheel] RAW EVENT: add ${path39}`);
@@ -9468,6 +9479,104 @@ function refreshIfStale(vaultPath2, index, excludeTags) {
9468
9479
  init_wikilinkFeedback();
9469
9480
  init_corrections();
9470
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
+ };
9471
9580
  var PIPELINE_TOTAL_STEPS = 22;
9472
9581
  function createEmptyPipelineActivity() {
9473
9582
  return {
@@ -9695,6 +9804,7 @@ var PipelineRunner = class {
9695
9804
  tracker.skip("entity_scan", `cache valid (${Math.round(entityScanAgeMs / 1e3)}s old)`);
9696
9805
  this.entitiesBefore = p.sd ? getAllEntitiesFromDb(p.sd) : [];
9697
9806
  this.entitiesAfter = this.entitiesBefore;
9807
+ p.deferredScheduler?.schedule("entity_scan", 5 * 60 * 1e3 - entityScanAgeMs);
9698
9808
  serverLog("watcher", `Entity scan: throttled (${Math.round(entityScanAgeMs / 1e3)}s old)`);
9699
9809
  return;
9700
9810
  }
@@ -9740,6 +9850,7 @@ var PipelineRunner = class {
9740
9850
  const { p } = this;
9741
9851
  const hubAgeMs = p.ctx.lastHubScoreRebuildAt > 0 ? Date.now() - p.ctx.lastHubScoreRebuildAt : Infinity;
9742
9852
  if (hubAgeMs < 5 * 60 * 1e3) {
9853
+ p.deferredScheduler?.schedule("hub_scores", 5 * 60 * 1e3 - hubAgeMs);
9743
9854
  serverLog("watcher", `Hub scores: throttled (${Math.round(hubAgeMs / 1e3)}s old)`);
9744
9855
  return { skipped: true, age_ms: hubAgeMs };
9745
9856
  }
@@ -9769,6 +9880,7 @@ var PipelineRunner = class {
9769
9880
  serverLog("watcher", `Recency: rebuilt ${recencyIndex2.lastMentioned.size} entities`);
9770
9881
  return { rebuilt: true, entities: recencyIndex2.lastMentioned.size };
9771
9882
  }
9883
+ p.deferredScheduler?.schedule("recency", 60 * 60 * 1e3 - cacheAgeMs);
9772
9884
  serverLog("watcher", `Recency: cache valid (${Math.round(cacheAgeMs / 1e3)}s old)`);
9773
9885
  return { rebuilt: false, cached_age_ms: cacheAgeMs };
9774
9886
  }
@@ -9788,6 +9900,7 @@ var PipelineRunner = class {
9788
9900
  serverLog("watcher", `Co-occurrence: rebuilt ${cooccurrenceIdx._metadata.total_associations} associations`);
9789
9901
  return { rebuilt: true, associations: cooccurrenceIdx._metadata.total_associations };
9790
9902
  }
9903
+ p.deferredScheduler?.schedule("cooccurrence", 60 * 60 * 1e3 - cooccurrenceAgeMs);
9791
9904
  serverLog("watcher", `Co-occurrence: cache valid (${Math.round(cooccurrenceAgeMs / 1e3)}s old)`);
9792
9905
  return { rebuilt: false, age_ms: cooccurrenceAgeMs };
9793
9906
  }
@@ -9810,6 +9923,7 @@ var PipelineRunner = class {
9810
9923
  top_changes: result.top_changes
9811
9924
  };
9812
9925
  }
9926
+ p.deferredScheduler?.schedule("edge_weights", 60 * 60 * 1e3 - edgeWeightAgeMs);
9813
9927
  serverLog("watcher", `Edge weights: cache valid (${Math.round(edgeWeightAgeMs / 1e3)}s old)`);
9814
9928
  return { rebuilt: false, age_ms: edgeWeightAgeMs };
9815
9929
  }
@@ -10799,7 +10913,7 @@ function getToolSelectionReport(stateDb2, daysBack = 7) {
10799
10913
  }
10800
10914
 
10801
10915
  // src/index.ts
10802
- 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";
10803
10917
 
10804
10918
  // src/core/write/memory.ts
10805
10919
  init_wikilinkFeedback();
@@ -11218,6 +11332,382 @@ function getSweepResults() {
11218
11332
  return cachedResults;
11219
11333
  }
11220
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
+
11221
11711
  // src/core/shared/metrics.ts
11222
11712
  init_wikilinkFeedback();
11223
11713
  var ALL_METRICS = [
@@ -11606,193 +12096,6 @@ function purgeOldInvocations(stateDb2, retentionDays = 90) {
11606
12096
  return result.changes;
11607
12097
  }
11608
12098
 
11609
- // src/core/shared/graphSnapshots.ts
11610
- function computeGraphMetrics(index) {
11611
- const noteCount = index.notes.size;
11612
- if (noteCount === 0) {
11613
- return {
11614
- avg_degree: 0,
11615
- max_degree: 0,
11616
- cluster_count: 0,
11617
- largest_cluster_size: 0,
11618
- hub_scores_top10: []
11619
- };
11620
- }
11621
- const degreeMap = /* @__PURE__ */ new Map();
11622
- const adjacency = /* @__PURE__ */ new Map();
11623
- for (const [notePath, note] of index.notes) {
11624
- if (!adjacency.has(notePath)) adjacency.set(notePath, /* @__PURE__ */ new Set());
11625
- let degree = note.outlinks.length;
11626
- for (const link of note.outlinks) {
11627
- const targetLower = link.target.toLowerCase();
11628
- const resolvedPath = index.entities.get(targetLower);
11629
- if (resolvedPath && index.notes.has(resolvedPath)) {
11630
- adjacency.get(notePath).add(resolvedPath);
11631
- if (!adjacency.has(resolvedPath)) adjacency.set(resolvedPath, /* @__PURE__ */ new Set());
11632
- adjacency.get(resolvedPath).add(notePath);
11633
- }
11634
- }
11635
- degreeMap.set(notePath, degree);
11636
- }
11637
- for (const [target, backlinks] of index.backlinks) {
11638
- const targetLower = target.toLowerCase();
11639
- const resolvedPath = index.entities.get(targetLower);
11640
- if (resolvedPath && degreeMap.has(resolvedPath)) {
11641
- degreeMap.set(resolvedPath, degreeMap.get(resolvedPath) + backlinks.length);
11642
- }
11643
- }
11644
- let totalDegree = 0;
11645
- let maxDegree = 0;
11646
- let maxDegreeNote = "";
11647
- for (const [notePath, degree] of degreeMap) {
11648
- totalDegree += degree;
11649
- if (degree > maxDegree) {
11650
- maxDegree = degree;
11651
- maxDegreeNote = notePath;
11652
- }
11653
- }
11654
- const avgDegree = noteCount > 0 ? Math.round(totalDegree / noteCount * 100) / 100 : 0;
11655
- const visited = /* @__PURE__ */ new Set();
11656
- const clusters = [];
11657
- for (const notePath of index.notes.keys()) {
11658
- if (visited.has(notePath)) continue;
11659
- const queue = [notePath];
11660
- visited.add(notePath);
11661
- let clusterSize = 0;
11662
- while (queue.length > 0) {
11663
- const current = queue.shift();
11664
- clusterSize++;
11665
- const neighbors = adjacency.get(current);
11666
- if (neighbors) {
11667
- for (const neighbor of neighbors) {
11668
- if (!visited.has(neighbor)) {
11669
- visited.add(neighbor);
11670
- queue.push(neighbor);
11671
- }
11672
- }
11673
- }
11674
- }
11675
- clusters.push(clusterSize);
11676
- }
11677
- const clusterCount = clusters.length;
11678
- const largestClusterSize = clusters.length > 0 ? Math.max(...clusters) : 0;
11679
- const sorted = Array.from(degreeMap.entries()).sort((a, b) => b[1] - a[1]).slice(0, 10);
11680
- const hubScoresTop10 = sorted.map(([notePath, degree]) => {
11681
- const note = index.notes.get(notePath);
11682
- return {
11683
- entity: note?.title ?? notePath,
11684
- degree
11685
- };
11686
- });
11687
- return {
11688
- avg_degree: avgDegree,
11689
- max_degree: maxDegree,
11690
- cluster_count: clusterCount,
11691
- largest_cluster_size: largestClusterSize,
11692
- hub_scores_top10: hubScoresTop10
11693
- };
11694
- }
11695
- function recordGraphSnapshot(stateDb2, metrics) {
11696
- const timestamp = Date.now();
11697
- const insert = stateDb2.db.prepare(
11698
- "INSERT INTO graph_snapshots (timestamp, metric, value, details) VALUES (?, ?, ?, ?)"
11699
- );
11700
- const transaction = stateDb2.db.transaction(() => {
11701
- insert.run(timestamp, "avg_degree", metrics.avg_degree, null);
11702
- insert.run(timestamp, "max_degree", metrics.max_degree, null);
11703
- insert.run(timestamp, "cluster_count", metrics.cluster_count, null);
11704
- insert.run(timestamp, "largest_cluster_size", metrics.largest_cluster_size, null);
11705
- insert.run(
11706
- timestamp,
11707
- "hub_scores_top10",
11708
- metrics.hub_scores_top10.length,
11709
- JSON.stringify(metrics.hub_scores_top10)
11710
- );
11711
- });
11712
- transaction();
11713
- }
11714
- function getEmergingHubs(stateDb2, daysBack = 30) {
11715
- const cutoff = Date.now() - daysBack * 24 * 60 * 60 * 1e3;
11716
- const latestRow = stateDb2.db.prepare(
11717
- `SELECT details FROM graph_snapshots
11718
- WHERE metric = 'hub_scores_top10'
11719
- ORDER BY timestamp DESC LIMIT 1`
11720
- ).get();
11721
- const previousRow = stateDb2.db.prepare(
11722
- `SELECT details FROM graph_snapshots
11723
- WHERE metric = 'hub_scores_top10' AND timestamp >= ?
11724
- ORDER BY timestamp ASC LIMIT 1`
11725
- ).get(cutoff);
11726
- if (!latestRow?.details) return [];
11727
- const currentHubs = JSON.parse(latestRow.details);
11728
- const previousHubs = previousRow?.details ? JSON.parse(previousRow.details) : [];
11729
- const previousMap = /* @__PURE__ */ new Map();
11730
- for (const hub of previousHubs) {
11731
- previousMap.set(hub.entity, hub.degree);
11732
- }
11733
- const emerging = currentHubs.map((hub) => {
11734
- const prevDegree = previousMap.get(hub.entity) ?? 0;
11735
- return {
11736
- entity: hub.entity,
11737
- current_degree: hub.degree,
11738
- previous_degree: prevDegree,
11739
- growth: hub.degree - prevDegree
11740
- };
11741
- });
11742
- emerging.sort((a, b) => b.growth - a.growth);
11743
- return emerging;
11744
- }
11745
- function compareGraphSnapshots(stateDb2, timestampBefore, timestampAfter) {
11746
- const SCALAR_METRICS = ["avg_degree", "max_degree", "cluster_count", "largest_cluster_size"];
11747
- function getSnapshotAt(ts) {
11748
- const row = stateDb2.db.prepare(
11749
- `SELECT DISTINCT timestamp FROM graph_snapshots WHERE timestamp <= ? ORDER BY timestamp DESC LIMIT 1`
11750
- ).get(ts);
11751
- if (!row) return null;
11752
- const rows = stateDb2.db.prepare(
11753
- `SELECT metric, value, details FROM graph_snapshots WHERE timestamp = ?`
11754
- ).all(row.timestamp);
11755
- return rows;
11756
- }
11757
- const beforeRows = getSnapshotAt(timestampBefore) ?? [];
11758
- const afterRows = getSnapshotAt(timestampAfter) ?? [];
11759
- const beforeMap = /* @__PURE__ */ new Map();
11760
- const afterMap = /* @__PURE__ */ new Map();
11761
- for (const r of beforeRows) beforeMap.set(r.metric, { value: r.value, details: r.details });
11762
- for (const r of afterRows) afterMap.set(r.metric, { value: r.value, details: r.details });
11763
- const metricChanges = SCALAR_METRICS.map((metric) => {
11764
- const before = beforeMap.get(metric)?.value ?? 0;
11765
- const after = afterMap.get(metric)?.value ?? 0;
11766
- const delta = after - before;
11767
- const deltaPercent = before !== 0 ? Math.round(delta / before * 1e4) / 100 : delta !== 0 ? 100 : 0;
11768
- return { metric, before, after, delta, deltaPercent };
11769
- });
11770
- const beforeHubs = beforeMap.get("hub_scores_top10")?.details ? JSON.parse(beforeMap.get("hub_scores_top10").details) : [];
11771
- const afterHubs = afterMap.get("hub_scores_top10")?.details ? JSON.parse(afterMap.get("hub_scores_top10").details) : [];
11772
- const beforeHubMap = /* @__PURE__ */ new Map();
11773
- for (const h of beforeHubs) beforeHubMap.set(h.entity, h.degree);
11774
- const afterHubMap = /* @__PURE__ */ new Map();
11775
- for (const h of afterHubs) afterHubMap.set(h.entity, h.degree);
11776
- const allHubEntities = /* @__PURE__ */ new Set([...beforeHubMap.keys(), ...afterHubMap.keys()]);
11777
- const hubScoreChanges = [];
11778
- for (const entity of allHubEntities) {
11779
- const before = beforeHubMap.get(entity) ?? 0;
11780
- const after = afterHubMap.get(entity) ?? 0;
11781
- if (before !== after) {
11782
- hubScoreChanges.push({ entity, before, after, delta: after - before });
11783
- }
11784
- }
11785
- hubScoreChanges.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
11786
- return { metricChanges, hubScoreChanges };
11787
- }
11788
- function purgeOldSnapshots(stateDb2, retentionDays = 90) {
11789
- const cutoff = Date.now() - retentionDays * 24 * 60 * 60 * 1e3;
11790
- const result = stateDb2.db.prepare(
11791
- "DELETE FROM graph_snapshots WHERE timestamp < ?"
11792
- ).run(cutoff);
11793
- return result.changes;
11794
- }
11795
-
11796
12099
  // src/index.ts
11797
12100
  init_serverLog();
11798
12101
  init_wikilinkFeedback();
@@ -12673,8 +12976,8 @@ var DEFAULT_CONFIG2 = {
12673
12976
  maxOutlinksPerHop: 10,
12674
12977
  maxBackfill: 10
12675
12978
  };
12676
- function multiHopBackfill(primaryResults, index, stateDb2, config = {}) {
12677
- const cfg = { ...DEFAULT_CONFIG2, ...config };
12979
+ function multiHopBackfill(primaryResults, index, stateDb2, config2 = {}) {
12980
+ const cfg = { ...DEFAULT_CONFIG2, ...config2 };
12678
12981
  const seen = new Set(primaryResults.map((r) => r.path).filter(Boolean));
12679
12982
  const candidates = [];
12680
12983
  const hop1Results = [];
@@ -15490,7 +15793,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
15490
15793
  }
15491
15794
  const indexBuilt = indexState2 === "ready" && index !== void 0 && index.notes !== void 0;
15492
15795
  let lastIndexActivityAt;
15493
- let lastFullRebuildAt;
15796
+ let lastFullRebuildAt2;
15494
15797
  let lastWatcherBatchAt;
15495
15798
  let lastBuild;
15496
15799
  let lastManual;
@@ -15500,13 +15803,13 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
15500
15803
  if (lastAny) lastIndexActivityAt = lastAny.timestamp;
15501
15804
  lastBuild = getLastEventByTrigger(stateDb2, "startup_build") ?? void 0;
15502
15805
  lastManual = getLastEventByTrigger(stateDb2, "manual_refresh") ?? void 0;
15503
- lastFullRebuildAt = Math.max(lastBuild?.timestamp ?? 0, lastManual?.timestamp ?? 0) || void 0;
15806
+ lastFullRebuildAt2 = Math.max(lastBuild?.timestamp ?? 0, lastManual?.timestamp ?? 0) || void 0;
15504
15807
  const lastWatcher = getLastEventByTrigger(stateDb2, "watcher");
15505
15808
  if (lastWatcher) lastWatcherBatchAt = lastWatcher.timestamp;
15506
15809
  } catch {
15507
15810
  }
15508
15811
  }
15509
- const freshnessTimestamp = lastFullRebuildAt ?? (indexBuilt && index.builtAt ? index.builtAt.getTime() : void 0);
15812
+ const freshnessTimestamp = lastFullRebuildAt2 ?? (indexBuilt && index.builtAt ? index.builtAt.getTime() : void 0);
15510
15813
  const indexAge = freshnessTimestamp ? Math.floor((Date.now() - freshnessTimestamp) / 1e3) : -1;
15511
15814
  const indexStale = indexBuilt && indexAge > STALE_THRESHOLD_SECONDS;
15512
15815
  if (indexState2 === "building") {
@@ -15553,8 +15856,8 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
15553
15856
  }
15554
15857
  let configInfo;
15555
15858
  if (isFull) {
15556
- const config = getConfig2();
15557
- configInfo = Object.keys(config).length > 0 ? config : void 0;
15859
+ const config2 = getConfig2();
15860
+ configInfo = Object.keys(config2).length > 0 ? config2 : void 0;
15558
15861
  }
15559
15862
  let lastRebuild;
15560
15863
  if (stateDb2) {
@@ -15683,15 +15986,15 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
15683
15986
  watcher_pending: getWatcherStatus2()?.pendingEvents,
15684
15987
  last_index_activity_at: lastIndexActivityAt,
15685
15988
  last_index_activity_ago_seconds: lastIndexActivityAt ? Math.floor((Date.now() - lastIndexActivityAt) / 1e3) : void 0,
15686
- last_full_rebuild_at: lastFullRebuildAt,
15989
+ last_full_rebuild_at: lastFullRebuildAt2,
15687
15990
  last_watcher_batch_at: lastWatcherBatchAt,
15688
15991
  pipeline_activity: pipelineActivity,
15689
15992
  dead_link_count: isFull ? deadLinkCount : void 0,
15690
15993
  top_dead_link_targets: isFull ? topDeadLinkTargets : void 0,
15691
15994
  sweep: isFull ? getSweepResults() ?? void 0 : void 0,
15692
15995
  proactive_linking: isFull && stateDb2 ? (() => {
15693
- const config = getConfig2();
15694
- const enabled = config.proactive_linking !== false;
15996
+ const config2 = getConfig2();
15997
+ const enabled = config2.proactive_linking !== false;
15695
15998
  const queuePending = stateDb2.db.prepare(
15696
15999
  `SELECT COUNT(*) as cnt FROM proactive_queue WHERE status = 'pending'`
15697
16000
  ).get();
@@ -16337,7 +16640,7 @@ function registerHealthTools(server2, getIndex, getVaultPath, getConfig2 = () =>
16337
16640
 
16338
16641
  // src/tools/read/system.ts
16339
16642
  import { z as z5 } from "zod";
16340
- 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";
16341
16644
 
16342
16645
  // src/core/read/aliasSuggestions.ts
16343
16646
  import { STOPWORDS_EN as STOPWORDS_EN3 } from "@velvetmonkey/vault-core";
@@ -16454,11 +16757,11 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
16454
16757
  if (stateDb2) {
16455
16758
  tracker.start("entity_sync", {});
16456
16759
  try {
16457
- const config = loadConfig(stateDb2);
16458
- const excludeFolders = config.exclude_entity_folders?.length ? config.exclude_entity_folders : DEFAULT_ENTITY_EXCLUDE_FOLDERS;
16459
- 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, {
16460
16763
  excludeFolders,
16461
- customCategories: config.custom_categories
16764
+ customCategories: config2.custom_categories
16462
16765
  });
16463
16766
  stateDb2.replaceAllEntities(entityIndex2);
16464
16767
  tracker.end({ entities: entityIndex2._metadata.total_entities });
@@ -16580,7 +16883,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
16580
16883
  if (stateDb2) {
16581
16884
  tracker.start("recency", {});
16582
16885
  try {
16583
- const entities = getAllEntitiesFromDb2(stateDb2).map((e) => ({
16886
+ const entities = getAllEntitiesFromDb3(stateDb2).map((e) => ({
16584
16887
  name: e.name,
16585
16888
  path: e.path,
16586
16889
  aliases: e.aliases
@@ -16599,7 +16902,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
16599
16902
  if (stateDb2) {
16600
16903
  tracker.start("cooccurrence", {});
16601
16904
  try {
16602
- const entityNames = getAllEntitiesFromDb2(stateDb2).map((e) => e.name);
16905
+ const entityNames = getAllEntitiesFromDb3(stateDb2).map((e) => e.name);
16603
16906
  const cooccurrenceIdx = await mineCooccurrences(vaultPath2, entityNames);
16604
16907
  setCooccurrenceIndex(cooccurrenceIdx);
16605
16908
  saveCooccurrenceToStateDb(stateDb2, cooccurrenceIdx);
@@ -16644,7 +16947,7 @@ function registerSystemTools(server2, getIndex, setIndex, getVaultPath, setConfi
16644
16947
  if (stateDb2 && hasEntityEmbeddingsIndex()) {
16645
16948
  tracker.start("entity_embeddings", {});
16646
16949
  try {
16647
- const entities = getAllEntitiesFromDb2(stateDb2);
16950
+ const entities = getAllEntitiesFromDb3(stateDb2);
16648
16951
  if (entities.length > 0) {
16649
16952
  const entityMap = new Map(entities.map((e) => [e.name, {
16650
16953
  name: e.name,
@@ -17071,9 +17374,9 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17071
17374
  const limit = Math.min(requestedLimit ?? 25, MAX_LIMIT);
17072
17375
  const index = getIndex();
17073
17376
  const vaultPath2 = getVaultPath();
17074
- const config = getConfig2();
17377
+ const config2 = getConfig2();
17075
17378
  if (path39) {
17076
- const result2 = await getTasksFromNote(index, path39, vaultPath2, getExcludeTags(config));
17379
+ const result2 = await getTasksFromNote(index, path39, vaultPath2, getExcludeTags(config2));
17077
17380
  if (!result2) {
17078
17381
  return {
17079
17382
  content: [{ type: "text", text: JSON.stringify({ error: "Note not found", path: path39 }, null, 2) }]
@@ -17096,12 +17399,12 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17096
17399
  };
17097
17400
  }
17098
17401
  if (isTaskCacheReady()) {
17099
- refreshIfStale(vaultPath2, index, getExcludeTags(config));
17402
+ refreshIfStale(vaultPath2, index, getExcludeTags(config2));
17100
17403
  if (has_due_date) {
17101
17404
  const result3 = queryTasksFromCache({
17102
17405
  status,
17103
17406
  folder,
17104
- excludeTags: getExcludeTags(config),
17407
+ excludeTags: getExcludeTags(config2),
17105
17408
  has_due_date: true,
17106
17409
  limit,
17107
17410
  offset
@@ -17118,7 +17421,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17118
17421
  status,
17119
17422
  folder,
17120
17423
  tag,
17121
- excludeTags: getExcludeTags(config),
17424
+ excludeTags: getExcludeTags(config2),
17122
17425
  limit,
17123
17426
  offset
17124
17427
  });
@@ -17137,7 +17440,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17137
17440
  const allResults = await getTasksWithDueDates(index, vaultPath2, {
17138
17441
  status,
17139
17442
  folder,
17140
- excludeTags: getExcludeTags(config)
17443
+ excludeTags: getExcludeTags(config2)
17141
17444
  });
17142
17445
  const paged2 = allResults.slice(offset, offset + limit);
17143
17446
  return {
@@ -17153,7 +17456,7 @@ function registerPrimitiveTools(server2, getIndex, getVaultPath, getConfig2 = ()
17153
17456
  folder,
17154
17457
  tag,
17155
17458
  limit: limit + offset,
17156
- excludeTags: getExcludeTags(config)
17459
+ excludeTags: getExcludeTags(config2)
17157
17460
  });
17158
17461
  const paged = result.tasks.slice(offset, offset + limit);
17159
17462
  return {
@@ -17820,10 +18123,10 @@ function isTemplatePath(notePath) {
17820
18123
  const folder = notePath.split("/")[0]?.toLowerCase() || "";
17821
18124
  return folder === "templates" || folder === "template";
17822
18125
  }
17823
- function getExcludedPaths(index, config) {
18126
+ function getExcludedPaths(index, config2) {
17824
18127
  const excluded = /* @__PURE__ */ new Set();
17825
- const excludeTags = new Set(getExcludeTags(config).map((t) => t.toLowerCase()));
17826
- 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()));
17827
18130
  if (excludeTags.size === 0 && excludeEntities.size === 0) return excluded;
17828
18131
  for (const note of index.notes.values()) {
17829
18132
  if (excludeTags.size > 0) {
@@ -17870,8 +18173,8 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb4
17870
18173
  requireIndex();
17871
18174
  const limit = Math.min(requestedLimit ?? 50, MAX_LIMIT);
17872
18175
  const index = getIndex();
17873
- const config = getConfig2?.() ?? {};
17874
- const excludedPaths = getExcludedPaths(index, config);
18176
+ const config2 = getConfig2?.() ?? {};
18177
+ const excludedPaths = getExcludedPaths(index, config2);
17875
18178
  switch (analysis) {
17876
18179
  case "orphans": {
17877
18180
  const allOrphans = findOrphanNotes(index, folder).filter((o) => !isPeriodicNote(o.path) && !excludedPaths.has(o.path));
@@ -18039,7 +18342,7 @@ function registerGraphAnalysisTools(server2, getIndex, getVaultPath, getStateDb4
18039
18342
  return !note || !excludedPaths.has(note.path);
18040
18343
  });
18041
18344
  }
18042
- const excludeEntities = new Set(getExcludeEntities(config).map((e) => e.toLowerCase()));
18345
+ const excludeEntities = new Set(getExcludeEntities(config2).map((e) => e.toLowerCase()));
18043
18346
  if (excludeEntities.size > 0) {
18044
18347
  hubs = hubs.filter((hub) => !excludeEntities.has(hub.entity.toLowerCase()));
18045
18348
  }
@@ -19678,14 +19981,14 @@ async function executeDeleteNote(options) {
19678
19981
  }
19679
19982
 
19680
19983
  // src/tools/write/mutations.ts
19681
- async function createNoteFromTemplate(vaultPath2, notePath, config) {
19984
+ async function createNoteFromTemplate(vaultPath2, notePath, config2) {
19682
19985
  const validation = await validatePathSecure(vaultPath2, notePath);
19683
19986
  if (!validation.valid) {
19684
19987
  throw new Error(`Path blocked: ${validation.reason}`);
19685
19988
  }
19686
19989
  const fullPath = path25.join(vaultPath2, notePath);
19687
19990
  await fs23.mkdir(path25.dirname(fullPath), { recursive: true });
19688
- const templates = config.templates || {};
19991
+ const templates = config2.templates || {};
19689
19992
  const filename = path25.basename(notePath, ".md").toLowerCase();
19690
19993
  let templatePath;
19691
19994
  let periodicType;
@@ -19798,8 +20101,8 @@ function registerMutationTools(server2, getVaultPath, getConfig2 = () => ({})) {
19798
20101
  try {
19799
20102
  await fs23.access(fullPath);
19800
20103
  } catch {
19801
- const config = getConfig2();
19802
- const result = await createNoteFromTemplate(vaultPath2, notePath, config);
20104
+ const config2 = getConfig2();
20105
+ const result = await createNoteFromTemplate(vaultPath2, notePath, config2);
19803
20106
  noteCreated = result.created;
19804
20107
  templateUsed = result.templateUsed;
19805
20108
  }
@@ -23905,9 +24208,9 @@ function registerConfigTools(server2, getConfig2, setConfig, getStateDb4) {
23905
24208
  async ({ mode, key, value }) => {
23906
24209
  switch (mode) {
23907
24210
  case "get": {
23908
- const config = getConfig2();
24211
+ const config2 = getConfig2();
23909
24212
  return {
23910
- content: [{ type: "text", text: JSON.stringify(config, null, 2) }]
24213
+ content: [{ type: "text", text: JSON.stringify(config2, null, 2) }]
23911
24214
  };
23912
24215
  }
23913
24216
  case "set": {
@@ -23958,7 +24261,7 @@ init_wikilinkFeedback();
23958
24261
  import { z as z28 } from "zod";
23959
24262
  import * as fs32 from "fs/promises";
23960
24263
  import * as path35 from "path";
23961
- 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";
23962
24265
  init_embeddings();
23963
24266
  function hasSkipWikilinks(content) {
23964
24267
  if (!content.startsWith("---")) return false;
@@ -24076,8 +24379,8 @@ async function executeRun(stateDb2, vaultPath2) {
24076
24379
  if (entityCount === 0) {
24077
24380
  const start = Date.now();
24078
24381
  try {
24079
- const config = loadConfig(stateDb2);
24080
- 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 });
24081
24384
  stateDb2.replaceAllEntities(entityIndex2);
24082
24385
  const newCount = entityIndex2._metadata.total_entities;
24083
24386
  steps.push({
@@ -24658,7 +24961,7 @@ function registerSimilarityTools(server2, getIndex, getVaultPath, getStateDb4) {
24658
24961
  // src/tools/read/semantic.ts
24659
24962
  init_embeddings();
24660
24963
  import { z as z31 } from "zod";
24661
- import { getAllEntitiesFromDb as getAllEntitiesFromDb3 } from "@velvetmonkey/vault-core";
24964
+ import { getAllEntitiesFromDb as getAllEntitiesFromDb4 } from "@velvetmonkey/vault-core";
24662
24965
  function registerSemanticTools(server2, getVaultPath, getStateDb4) {
24663
24966
  server2.registerTool(
24664
24967
  "init_semantic",
@@ -24723,7 +25026,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb4) {
24723
25026
  const embedded = progress.total - progress.skipped;
24724
25027
  let entityEmbedded = 0;
24725
25028
  try {
24726
- const allEntities = getAllEntitiesFromDb3(stateDb2);
25029
+ const allEntities = getAllEntitiesFromDb4(stateDb2);
24727
25030
  const entityMap = /* @__PURE__ */ new Map();
24728
25031
  for (const e of allEntities) {
24729
25032
  entityMap.set(e.name, {
@@ -24785,7 +25088,7 @@ function registerSemanticTools(server2, getVaultPath, getStateDb4) {
24785
25088
  // src/tools/read/merges.ts
24786
25089
  init_levenshtein();
24787
25090
  import { z as z32 } from "zod";
24788
- import { getAllEntitiesFromDb as getAllEntitiesFromDb4, getDismissedMergePairs, recordMergeDismissal } from "@velvetmonkey/vault-core";
25091
+ import { getAllEntitiesFromDb as getAllEntitiesFromDb5, getDismissedMergePairs, recordMergeDismissal } from "@velvetmonkey/vault-core";
24789
25092
  function normalizeName(name) {
24790
25093
  return name.toLowerCase().replace(/[.\-_]/g, "").replace(/js$/, "").replace(/ts$/, "");
24791
25094
  }
@@ -24803,7 +25106,7 @@ function registerMergeTools2(server2, getStateDb4) {
24803
25106
  content: [{ type: "text", text: JSON.stringify({ suggestions: [], error: "StateDb not available" }) }]
24804
25107
  };
24805
25108
  }
24806
- const entities = getAllEntitiesFromDb4(stateDb2);
25109
+ const entities = getAllEntitiesFromDb5(stateDb2);
24807
25110
  if (entities.length === 0) {
24808
25111
  return {
24809
25112
  content: [{ type: "text", text: JSON.stringify({ suggestions: [] }) }]
@@ -26286,7 +26589,7 @@ function queryFlywheelAgeDays(stateDb2) {
26286
26589
  if (!row?.first_ts) return 0;
26287
26590
  return Math.floor((Date.now() - row.first_ts) / (24 * 60 * 60 * 1e3));
26288
26591
  }
26289
- function getCalibrationExport(stateDb2, metrics, config, daysBack = 30, includeVaultId = true) {
26592
+ function getCalibrationExport(stateDb2, metrics, config2, daysBack = 30, includeVaultId = true) {
26290
26593
  const now = /* @__PURE__ */ new Date();
26291
26594
  const start = new Date(now);
26292
26595
  start.setDate(start.getDate() - daysBack + 1);
@@ -26309,8 +26612,8 @@ function getCalibrationExport(stateDb2, metrics, config, daysBack = 30, includeV
26309
26612
  connected_ratio: round(metrics.connected_ratio),
26310
26613
  semantic_enabled: hasEmbeddingsIndex(),
26311
26614
  flywheel_age_days: queryFlywheelAgeDays(stateDb2),
26312
- strictness_mode: config.wikilink_strictness ?? "balanced",
26313
- adaptive_strictness: config.adaptive_strictness ?? true
26615
+ strictness_mode: config2.wikilink_strictness ?? "balanced",
26616
+ adaptive_strictness: config2.adaptive_strictness ?? true
26314
26617
  },
26315
26618
  entity_distribution: queryEntityDistribution(stateDb2),
26316
26619
  funnel: queryFunnel2(stateDb2, startMs, startIso, endIso),
@@ -26344,11 +26647,11 @@ function registerCalibrationExportTools(server2, getIndex, getStateDb4, getConfi
26344
26647
  }
26345
26648
  const index = getIndex();
26346
26649
  const metrics = computeMetrics(index, stateDb2);
26347
- const config = getConfig2();
26650
+ const config2 = getConfig2();
26348
26651
  const report = getCalibrationExport(
26349
26652
  stateDb2,
26350
26653
  metrics,
26351
- config,
26654
+ config2,
26352
26655
  args.days_back ?? 30,
26353
26656
  args.include_vault_id ?? true
26354
26657
  );
@@ -26602,7 +26905,7 @@ function extractSearchMethod(result) {
26602
26905
  }
26603
26906
  return void 0;
26604
26907
  }
26605
- 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) {
26606
26909
  let _registered = 0;
26607
26910
  let _skipped = 0;
26608
26911
  let tierOverride = "auto";
@@ -26695,6 +26998,7 @@ function applyToolGating(targetServer, categories, getDb4, registry, getVaultPat
26695
26998
  }
26696
26999
  function wrapWithTracking(toolName, handler) {
26697
27000
  return async (...args) => {
27001
+ onToolCall?.();
26698
27002
  const start = Date.now();
26699
27003
  let success = true;
26700
27004
  let notePaths;
@@ -27114,6 +27418,9 @@ var httpListener = null;
27114
27418
  var watchdogTimer = null;
27115
27419
  var serverReady = false;
27116
27420
  var shutdownRequested = false;
27421
+ var lastMcpRequestAt = 0;
27422
+ var lastFullRebuildAt = 0;
27423
+ var deferredScheduler = null;
27117
27424
  function getWatcherStatus() {
27118
27425
  if (vaultRegistry) {
27119
27426
  const name = globalThis.__flywheel_active_vault;
@@ -27147,8 +27454,8 @@ function handleTierStateChange(controller) {
27147
27454
  syncRuntimeTierState(controller);
27148
27455
  invalidateHttpPool();
27149
27456
  }
27150
- function getConfigToolTierOverride(config) {
27151
- return config.tool_tier_override ?? "auto";
27457
+ function getConfigToolTierOverride(config2) {
27458
+ return config2.tool_tier_override ?? "auto";
27152
27459
  }
27153
27460
  function buildRegistryContext() {
27154
27461
  return {
@@ -27180,7 +27487,10 @@ function createConfiguredServer() {
27180
27487
  buildVaultCallbacks(),
27181
27488
  toolTierMode,
27182
27489
  handleTierStateChange,
27183
- toolConfig.isFullToolset
27490
+ toolConfig.isFullToolset,
27491
+ () => {
27492
+ lastMcpRequestAt = Date.now();
27493
+ }
27184
27494
  );
27185
27495
  registerAllTools(s, ctx, toolTierController);
27186
27496
  toolTierController.setOverride(runtimeToolTierOverride);
@@ -27234,7 +27544,10 @@ var _gatingResult = applyToolGating(
27234
27544
  buildVaultCallbacks(),
27235
27545
  toolTierMode,
27236
27546
  handleTierStateChange,
27237
- toolConfig.isFullToolset
27547
+ toolConfig.isFullToolset,
27548
+ () => {
27549
+ lastMcpRequestAt = Date.now();
27550
+ }
27238
27551
  );
27239
27552
  registerAllTools(server, _registryCtx, _gatingResult);
27240
27553
  _gatingResult.setOverride(runtimeToolTierOverride);
@@ -27357,17 +27670,17 @@ function updateVaultIndex(index) {
27357
27670
  const ctx = getActiveVaultContext();
27358
27671
  if (ctx) ctx.vaultIndex = index;
27359
27672
  }
27360
- function updateFlywheelConfig(config) {
27361
- flywheelConfig = config;
27362
- setWikilinkConfig(config);
27673
+ function updateFlywheelConfig(config2) {
27674
+ flywheelConfig = config2;
27675
+ setWikilinkConfig(config2);
27363
27676
  if (toolTierMode === "tiered" && primaryToolTierController) {
27364
- primaryToolTierController.setOverride(getConfigToolTierOverride(config));
27677
+ primaryToolTierController.setOverride(getConfigToolTierOverride(config2));
27365
27678
  syncRuntimeTierState(primaryToolTierController);
27366
27679
  invalidateHttpPool();
27367
27680
  }
27368
27681
  const ctx = getActiveVaultContext();
27369
27682
  if (ctx) {
27370
- ctx.flywheelConfig = config;
27683
+ ctx.flywheelConfig = config2;
27371
27684
  setActiveScope(buildVaultScope(ctx));
27372
27685
  }
27373
27686
  }
@@ -27422,6 +27735,7 @@ async function bootVault(ctx, startTime) {
27422
27735
  note_count: cachedIndex.notes.size
27423
27736
  });
27424
27737
  }
27738
+ lastFullRebuildAt = Date.now();
27425
27739
  await runPostIndexWork(ctx);
27426
27740
  } else {
27427
27741
  serverLog("index", `[${ctx.name}] Cache miss: building from scratch`);
@@ -27446,6 +27760,7 @@ async function bootVault(ctx, startTime) {
27446
27760
  serverLog("index", `[${ctx.name}] Failed to save index cache: ${err instanceof Error ? err.message : err}`, "error");
27447
27761
  }
27448
27762
  }
27763
+ lastFullRebuildAt = Date.now();
27449
27764
  await runPostIndexWork(ctx);
27450
27765
  } catch (err) {
27451
27766
  updateIndexState("error", err instanceof Error ? err : new Error(String(err)));
@@ -27666,11 +27981,11 @@ async function updateEntitiesInStateDb(vp, sd) {
27666
27981
  const vault = vp ?? vaultPath;
27667
27982
  if (!db4) return;
27668
27983
  try {
27669
- const config = loadConfig(db4);
27670
- const excludeFolders = config.exclude_entity_folders?.length ? config.exclude_entity_folders : DEFAULT_ENTITY_EXCLUDE_FOLDERS;
27671
- 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, {
27672
27987
  excludeFolders,
27673
- customCategories: config.custom_categories
27988
+ customCategories: config2.custom_categories
27674
27989
  });
27675
27990
  db4.replaceAllEntities(entityIndex2);
27676
27991
  serverLog("index", `Updated ${entityIndex2._metadata.total_entities} entities in StateDb`);
@@ -27817,7 +28132,7 @@ async function runPostIndexWork(ctx) {
27817
28132
  serverLog("semantic", "Embeddings up-to-date, skipping build");
27818
28133
  loadEntityEmbeddingsToMemory();
27819
28134
  if (sd) {
27820
- const entities = getAllEntitiesFromDb5(sd);
28135
+ const entities = getAllEntitiesFromDb6(sd);
27821
28136
  if (entities.length > 0) {
27822
28137
  saveInferredCategories(classifyUncategorizedEntities(
27823
28138
  entities.map((entity) => ({
@@ -27854,7 +28169,7 @@ async function runPostIndexWork(ctx) {
27854
28169
  }
27855
28170
  });
27856
28171
  if (sd) {
27857
- const entities = getAllEntitiesFromDb5(sd);
28172
+ const entities = getAllEntitiesFromDb6(sd);
27858
28173
  if (entities.length > 0) {
27859
28174
  const entityMap = new Map(entities.map((e) => [e.name, {
27860
28175
  name: e.name,
@@ -27869,7 +28184,7 @@ async function runPostIndexWork(ctx) {
27869
28184
  activateVault(ctx);
27870
28185
  loadEntityEmbeddingsToMemory();
27871
28186
  if (sd) {
27872
- const entities = getAllEntitiesFromDb5(sd);
28187
+ const entities = getAllEntitiesFromDb6(sd);
27873
28188
  if (entities.length > 0) {
27874
28189
  saveInferredCategories(classifyUncategorizedEntities(
27875
28190
  entities.map((entity) => ({
@@ -27907,7 +28222,7 @@ async function runPostIndexWork(ctx) {
27907
28222
  serverLog("semantic", "Skipping \u2014 FLYWHEEL_SKIP_EMBEDDINGS");
27908
28223
  }
27909
28224
  if (process.env.FLYWHEEL_WATCH !== "false") {
27910
- const config = parseWatcherConfig();
28225
+ const config2 = parseWatcherConfig();
27911
28226
  const lastContentHashes = /* @__PURE__ */ new Map();
27912
28227
  if (sd) {
27913
28228
  const persisted = loadContentHashes(sd);
@@ -27916,7 +28231,15 @@ async function runPostIndexWork(ctx) {
27916
28231
  serverLog("watcher", `Loaded ${persisted.size} persisted content hashes`);
27917
28232
  }
27918
28233
  }
27919
- 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
+ });
27920
28243
  const handleBatch = async (batch) => {
27921
28244
  const vaultPrefixes = /* @__PURE__ */ new Set([
27922
28245
  normalizePath(vp),
@@ -28057,13 +28380,14 @@ async function runPostIndexWork(ctx) {
28057
28380
  updateVaultIndex,
28058
28381
  updateEntitiesInStateDb,
28059
28382
  getVaultIndex: () => vaultIndex,
28060
- buildVaultIndex
28383
+ buildVaultIndex,
28384
+ deferredScheduler: deferredScheduler ?? void 0
28061
28385
  });
28062
28386
  await runner.run();
28063
28387
  };
28064
28388
  const watcher = createVaultWatcher({
28065
28389
  vaultPath: vp,
28066
- config,
28390
+ config: config2,
28067
28391
  onBatch: handleBatch,
28068
28392
  onStateChange: (status) => {
28069
28393
  if (status.state === "dirty") {
@@ -28104,6 +28428,18 @@ async function runPostIndexWork(ctx) {
28104
28428
  if (sd) runPeriodicMaintenance(sd);
28105
28429
  });
28106
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)`);
28107
28443
  }
28108
28444
  const postDuration = Date.now() - postStart;
28109
28445
  serverLog("server", `Post-index work complete in ${postDuration}ms`);
@@ -28151,7 +28487,12 @@ function gracefulShutdown(signal) {
28151
28487
  watcherInstance?.stop();
28152
28488
  } catch {
28153
28489
  }
28490
+ try {
28491
+ deferredScheduler?.cancelAll();
28492
+ } catch {
28493
+ }
28154
28494
  stopSweepTimer();
28495
+ stopMaintenanceTimer();
28155
28496
  flushLogs().catch(() => {
28156
28497
  }).finally(() => process.exit(0));
28157
28498
  setTimeout(() => process.exit(0), 2e3).unref();