@karmaniverous/jeeves-watcher 0.2.2 → 0.2.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/mjs/index.js CHANGED
@@ -644,6 +644,16 @@ const jeevesWatcherConfigSchema = z.object({
644
644
  .number()
645
645
  .optional()
646
646
  .describe('Timeout in milliseconds for graceful shutdown.'),
647
+ /** Maximum consecutive system-level failures before triggering fatal error. Default: Infinity. */
648
+ maxRetries: z
649
+ .number()
650
+ .optional()
651
+ .describe('Maximum consecutive system-level failures before triggering fatal error. Default: Infinity.'),
652
+ /** Maximum backoff delay in milliseconds for system errors. Default: 60000. */
653
+ maxBackoffMs: z
654
+ .number()
655
+ .optional()
656
+ .describe('Maximum backoff delay in milliseconds for system errors. Default: 60000.'),
647
657
  });
648
658
 
649
659
  /**
@@ -656,15 +666,13 @@ const ENV_PATTERN = /\$\{([^}]+)\}/g;
656
666
  * Replace `${VAR_NAME}` patterns in a string with `process.env.VAR_NAME`.
657
667
  *
658
668
  * @param value - The string to process.
659
- * @returns The string with env vars substituted.
660
- * @throws If a referenced env var is not set.
669
+ * @returns The string with resolved env vars; unresolvable expressions left untouched.
661
670
  */
662
671
  function substituteString(value) {
663
672
  return value.replace(ENV_PATTERN, (match, varName) => {
664
673
  const envValue = process.env[varName];
665
- if (envValue === undefined) {
666
- throw new Error(`Environment variable \${${varName}} referenced in config is not set.`);
667
- }
674
+ if (envValue === undefined)
675
+ return match;
668
676
  return envValue;
669
677
  });
670
678
  }
@@ -1050,11 +1058,11 @@ async function extractMarkdown(filePath) {
1050
1058
  }
1051
1059
  async function extractPlaintext(filePath) {
1052
1060
  const raw = await readFile(filePath, 'utf8');
1053
- return { text: raw };
1061
+ return { text: raw.replace(/^\uFEFF/, '') };
1054
1062
  }
1055
1063
  async function extractJson(filePath) {
1056
1064
  const raw = await readFile(filePath, 'utf8');
1057
- const parsed = JSON.parse(raw);
1065
+ const parsed = JSON.parse(raw.replace(/^\uFEFF/, ''));
1058
1066
  const json = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
1059
1067
  ? parsed
1060
1068
  : undefined;
@@ -1076,7 +1084,7 @@ async function extractDocx(filePath) {
1076
1084
  }
1077
1085
  async function extractHtml(filePath) {
1078
1086
  const raw = await readFile(filePath, 'utf8');
1079
- const $ = cheerio.load(raw);
1087
+ const $ = cheerio.load(raw.replace(/^\uFEFF/, ''));
1080
1088
  $('script, style').remove();
1081
1089
  const text = $('body').text().trim() || $.text().trim();
1082
1090
  return { text };
@@ -1906,6 +1914,112 @@ class VectorStoreClient {
1906
1914
  }
1907
1915
  }
1908
1916
 
1917
+ /**
1918
+ * @module health
1919
+ * Tracks consecutive system-level failures and applies exponential backoff.
1920
+ * Triggers fatal error callback when maxRetries is exceeded.
1921
+ */
1922
+ /**
1923
+ * Tracks system health via consecutive failure count and exponential backoff.
1924
+ */
1925
+ class SystemHealth {
1926
+ consecutiveFailures = 0;
1927
+ maxRetries;
1928
+ maxBackoffMs;
1929
+ baseDelayMs;
1930
+ onFatalError;
1931
+ logger;
1932
+ constructor(options) {
1933
+ this.maxRetries = options.maxRetries ?? Number.POSITIVE_INFINITY;
1934
+ this.maxBackoffMs = options.maxBackoffMs ?? 60_000;
1935
+ this.baseDelayMs = options.baseDelayMs ?? 1000;
1936
+ this.onFatalError = options.onFatalError;
1937
+ this.logger = options.logger;
1938
+ }
1939
+ /**
1940
+ * Record a successful system operation. Resets the failure counter.
1941
+ */
1942
+ recordSuccess() {
1943
+ if (this.consecutiveFailures > 0) {
1944
+ this.logger.info({ previousFailures: this.consecutiveFailures }, 'System health recovered');
1945
+ }
1946
+ this.consecutiveFailures = 0;
1947
+ }
1948
+ /**
1949
+ * Record a system-level failure. If maxRetries is exceeded, triggers fatal error.
1950
+ *
1951
+ * @param error - The error that occurred.
1952
+ * @returns Whether the watcher should continue (false = fatal).
1953
+ */
1954
+ recordFailure(error) {
1955
+ this.consecutiveFailures += 1;
1956
+ this.logger.error({
1957
+ consecutiveFailures: this.consecutiveFailures,
1958
+ maxRetries: this.maxRetries,
1959
+ err: normalizeError(error),
1960
+ }, 'System-level failure recorded');
1961
+ if (this.consecutiveFailures >= this.maxRetries) {
1962
+ this.logger.fatal({ consecutiveFailures: this.consecutiveFailures }, 'Maximum retries exceeded, triggering fatal error');
1963
+ if (this.onFatalError) {
1964
+ this.onFatalError(error);
1965
+ return false;
1966
+ }
1967
+ throw error instanceof Error
1968
+ ? error
1969
+ : new Error(`Fatal system error: ${String(error)}`);
1970
+ }
1971
+ return true;
1972
+ }
1973
+ /**
1974
+ * Compute the current backoff delay based on consecutive failures.
1975
+ *
1976
+ * @returns Delay in milliseconds.
1977
+ */
1978
+ get currentBackoffMs() {
1979
+ if (this.consecutiveFailures === 0)
1980
+ return 0;
1981
+ const exp = Math.max(0, this.consecutiveFailures - 1);
1982
+ return Math.min(this.maxBackoffMs, this.baseDelayMs * 2 ** exp);
1983
+ }
1984
+ /**
1985
+ * Sleep for the current backoff duration.
1986
+ *
1987
+ * @param signal - Optional abort signal.
1988
+ */
1989
+ async backoff(signal) {
1990
+ const delay = this.currentBackoffMs;
1991
+ if (delay <= 0)
1992
+ return;
1993
+ this.logger.warn({ delayMs: delay, consecutiveFailures: this.consecutiveFailures }, 'Backing off before next attempt');
1994
+ await new Promise((resolve, reject) => {
1995
+ const timer = setTimeout(() => {
1996
+ cleanup();
1997
+ resolve();
1998
+ }, delay);
1999
+ const onAbort = () => {
2000
+ cleanup();
2001
+ reject(new Error('Backoff aborted'));
2002
+ };
2003
+ const cleanup = () => {
2004
+ clearTimeout(timer);
2005
+ if (signal)
2006
+ signal.removeEventListener('abort', onAbort);
2007
+ };
2008
+ if (signal) {
2009
+ if (signal.aborted) {
2010
+ onAbort();
2011
+ return;
2012
+ }
2013
+ signal.addEventListener('abort', onAbort, { once: true });
2014
+ }
2015
+ });
2016
+ }
2017
+ /** Current consecutive failure count. */
2018
+ get failures() {
2019
+ return this.consecutiveFailures;
2020
+ }
2021
+ }
2022
+
1909
2023
  /**
1910
2024
  * @module watcher
1911
2025
  * Filesystem watcher wrapping chokidar. I/O: watches files/directories for add/change/unlink events, enqueues to processing queue.
@@ -1918,6 +2032,7 @@ class FileSystemWatcher {
1918
2032
  queue;
1919
2033
  processor;
1920
2034
  logger;
2035
+ health;
1921
2036
  watcher;
1922
2037
  /**
1923
2038
  * Create a new FileSystemWatcher.
@@ -1926,12 +2041,20 @@ class FileSystemWatcher {
1926
2041
  * @param queue - The event queue.
1927
2042
  * @param processor - The document processor.
1928
2043
  * @param logger - The logger instance.
2044
+ * @param options - Optional health/fatal error options.
1929
2045
  */
1930
- constructor(config, queue, processor, logger) {
2046
+ constructor(config, queue, processor, logger, options = {}) {
1931
2047
  this.config = config;
1932
2048
  this.queue = queue;
1933
2049
  this.processor = processor;
1934
2050
  this.logger = logger;
2051
+ const healthOptions = {
2052
+ maxRetries: options.maxRetries,
2053
+ maxBackoffMs: options.maxBackoffMs,
2054
+ onFatalError: options.onFatalError,
2055
+ logger,
2056
+ };
2057
+ this.health = new SystemHealth(healthOptions);
1935
2058
  }
1936
2059
  /**
1937
2060
  * Start watching the filesystem and processing events.
@@ -1948,18 +2071,19 @@ class FileSystemWatcher {
1948
2071
  });
1949
2072
  this.watcher.on('add', (path) => {
1950
2073
  this.logger.debug({ path }, 'File added');
1951
- this.queue.enqueue({ type: 'create', path, priority: 'normal' }, () => this.processor.processFile(path));
2074
+ this.queue.enqueue({ type: 'create', path, priority: 'normal' }, () => this.wrapProcessing(() => this.processor.processFile(path)));
1952
2075
  });
1953
2076
  this.watcher.on('change', (path) => {
1954
2077
  this.logger.debug({ path }, 'File changed');
1955
- this.queue.enqueue({ type: 'modify', path, priority: 'normal' }, () => this.processor.processFile(path));
2078
+ this.queue.enqueue({ type: 'modify', path, priority: 'normal' }, () => this.wrapProcessing(() => this.processor.processFile(path)));
1956
2079
  });
1957
2080
  this.watcher.on('unlink', (path) => {
1958
2081
  this.logger.debug({ path }, 'File removed');
1959
- this.queue.enqueue({ type: 'delete', path, priority: 'normal' }, () => this.processor.deleteFile(path));
2082
+ this.queue.enqueue({ type: 'delete', path, priority: 'normal' }, () => this.wrapProcessing(() => this.processor.deleteFile(path)));
1960
2083
  });
1961
2084
  this.watcher.on('error', (error) => {
1962
2085
  this.logger.error({ err: normalizeError(error) }, 'Watcher error');
2086
+ this.health.recordFailure(error);
1963
2087
  });
1964
2088
  this.queue.process();
1965
2089
  this.logger.info({ paths: this.config.paths }, 'Filesystem watcher started');
@@ -1974,6 +2098,30 @@ class FileSystemWatcher {
1974
2098
  this.logger.info('Filesystem watcher stopped');
1975
2099
  }
1976
2100
  }
2101
+ /**
2102
+ * Get the system health tracker.
2103
+ */
2104
+ get systemHealth() {
2105
+ return this.health;
2106
+ }
2107
+ /**
2108
+ * Wrap a processing operation with health tracking.
2109
+ * On success, resets the failure counter.
2110
+ * On failure, records the failure and applies backoff.
2111
+ */
2112
+ async wrapProcessing(fn) {
2113
+ try {
2114
+ await this.health.backoff();
2115
+ await fn();
2116
+ this.health.recordSuccess();
2117
+ }
2118
+ catch (error) {
2119
+ const shouldContinue = this.health.recordFailure(error);
2120
+ if (!shouldContinue) {
2121
+ await this.stop();
2122
+ }
2123
+ }
2124
+ }
1977
2125
  }
1978
2126
 
1979
2127
  /**
@@ -2049,7 +2197,7 @@ const defaultFactories = {
2049
2197
  compileRules,
2050
2198
  createDocumentProcessor: (config, embeddingProvider, vectorStore, compiledRules, logger) => new DocumentProcessor(config, embeddingProvider, vectorStore, compiledRules, logger),
2051
2199
  createEventQueue: (options) => new EventQueue(options),
2052
- createFileSystemWatcher: (config, queue, processor, logger) => new FileSystemWatcher(config, queue, processor, logger),
2200
+ createFileSystemWatcher: (config, queue, processor, logger, options) => new FileSystemWatcher(config, queue, processor, logger, options),
2053
2201
  createApiServer,
2054
2202
  };
2055
2203
  /**
@@ -2059,6 +2207,7 @@ class JeevesWatcher {
2059
2207
  config;
2060
2208
  configPath;
2061
2209
  factories;
2210
+ runtimeOptions;
2062
2211
  logger;
2063
2212
  watcher;
2064
2213
  queue;
@@ -2071,11 +2220,13 @@ class JeevesWatcher {
2071
2220
  * @param config - The application configuration.
2072
2221
  * @param configPath - Optional config file path to watch for changes.
2073
2222
  * @param factories - Optional component factories (for dependency injection).
2223
+ * @param runtimeOptions - Optional runtime-only options (e.g., onFatalError).
2074
2224
  */
2075
- constructor(config, configPath, factories = {}) {
2225
+ constructor(config, configPath, factories = {}, runtimeOptions = {}) {
2076
2226
  this.config = config;
2077
2227
  this.configPath = configPath;
2078
2228
  this.factories = { ...defaultFactories, ...factories };
2229
+ this.runtimeOptions = runtimeOptions;
2079
2230
  }
2080
2231
  /**
2081
2232
  * Start the watcher, API server, and all components.
@@ -2108,7 +2259,11 @@ class JeevesWatcher {
2108
2259
  rateLimitPerMinute: this.config.embedding.rateLimitPerMinute,
2109
2260
  });
2110
2261
  this.queue = queue;
2111
- const watcher = this.factories.createFileSystemWatcher(this.config.watch, queue, processor, logger);
2262
+ const watcher = this.factories.createFileSystemWatcher(this.config.watch, queue, processor, logger, {
2263
+ maxRetries: this.config.maxRetries,
2264
+ maxBackoffMs: this.config.maxBackoffMs,
2265
+ onFatalError: this.runtimeOptions.onFatalError,
2266
+ });
2112
2267
  this.watcher = watcher;
2113
2268
  const server = this.factories.createApiServer({
2114
2269
  processor,
@@ -2213,4 +2368,4 @@ async function startFromConfig(configPath) {
2213
2368
  return app;
2214
2369
  }
2215
2370
 
2216
- export { DocumentProcessor, EventQueue, FileSystemWatcher, JeevesWatcher, VectorStoreClient, apiConfigSchema, applyRules, buildAttributes, compileRules, configWatchConfigSchema, contentHash, createApiServer, createEmbeddingProvider, createLogger, deleteMetadata, embeddingConfigSchema, extractText, inferenceRuleSchema, jeevesWatcherConfigSchema, loadConfig, loggingConfigSchema, metadataPath, pointId, readMetadata, startFromConfig, vectorStoreConfigSchema, watchConfigSchema, writeMetadata };
2371
+ export { DocumentProcessor, EventQueue, FileSystemWatcher, JeevesWatcher, SystemHealth, VectorStoreClient, apiConfigSchema, applyRules, buildAttributes, compileRules, configWatchConfigSchema, contentHash, createApiServer, createEmbeddingProvider, createLogger, deleteMetadata, embeddingConfigSchema, extractText, inferenceRuleSchema, jeevesWatcherConfigSchema, loadConfig, loggingConfigSchema, metadataPath, pointId, readMetadata, startFromConfig, vectorStoreConfigSchema, watchConfigSchema, writeMetadata };
package/package.json CHANGED
@@ -171,5 +171,5 @@
171
171
  },
172
172
  "type": "module",
173
173
  "types": "dist/index.d.ts",
174
- "version": "0.2.2"
174
+ "version": "0.2.4"
175
175
  }